Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad4418715 | ||
|
|
58d17912e8 | ||
|
|
2c6d92fd80 | ||
|
|
e233218359 | ||
|
|
05b61f812a | ||
|
|
e0c421ab71 | ||
|
|
2ee6147510 | ||
|
|
e02c173fa5 | ||
|
|
24a2ba03fb | ||
|
|
012b371cde | ||
|
|
83c88ad470 | ||
|
|
cad813ac1e |
@@ -2,4 +2,4 @@ import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies Partial<PluginOptions>);
|
||||
} satisfies PluginOptions);
|
||||
@@ -263,7 +263,7 @@ test('error is displayed when config has validation error', async ({page}) => {
|
||||
|
||||
({
|
||||
compilationMode: "123"
|
||||
} satisfies Partial<PluginOptions>);`,
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
@@ -293,7 +293,7 @@ test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
environment: {
|
||||
disableMemoizationForDebugging: true
|
||||
}
|
||||
} satisfies Partial<PluginOptions>);`,
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
|
||||
@@ -240,7 +240,7 @@ export function addImportsToProgram(
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const existingImports = getExistingImports(path);
|
||||
const stmts: Array<t.ImportDeclaration> = [];
|
||||
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
|
||||
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
@@ -303,9 +303,29 @@ export function addImportsToProgram(
|
||||
if (maybeExistingImports != null) {
|
||||
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
if (path.node.sourceType === 'module') {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.objectPattern(
|
||||
sortedImport.map(specifier => {
|
||||
return t.objectProperty(
|
||||
t.identifier(specifier.imported),
|
||||
t.identifier(specifier.name),
|
||||
);
|
||||
}),
|
||||
),
|
||||
t.callExpression(t.identifier('require'), [
|
||||
t.stringLiteral(moduleName),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
|
||||
@@ -103,7 +103,6 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
@@ -276,10 +275,6 @@ function runWithEnvironment(
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
@@ -334,12 +334,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
|
||||
* during render. Generates a custom error message for each type of violation.
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
|
||||
@@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
const parentName = fn.id;
|
||||
const functions = nameAnonymousFunctionsImpl(fn);
|
||||
function visit(node: Node, prefix: string): void {
|
||||
if (node.generatedName != null) {
|
||||
if (node.generatedName != null && node.fn.nameHint == null) {
|
||||
/**
|
||||
* Note that we don't generate a name for functions that already had one,
|
||||
* so we'll only add the prefix to anonymous functions regardless of
|
||||
@@ -70,6 +70,10 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
if (name != null && name.kind === 'named') {
|
||||
names.set(lvalue.identifier.id, name.value);
|
||||
}
|
||||
const func = functions.get(value.place.identifier.id);
|
||||
if (func != null) {
|
||||
functions.set(lvalue.identifier.id, func);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
@@ -106,6 +110,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
const variableName = value.lvalue.place.identifier.name;
|
||||
if (
|
||||
node != null &&
|
||||
node.generatedName == null &&
|
||||
variableName != null &&
|
||||
variableName.kind === 'named'
|
||||
) {
|
||||
@@ -137,7 +142,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(arg.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const generatedName =
|
||||
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
|
||||
node.generatedName = generatedName;
|
||||
@@ -152,7 +157,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(attr.place.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const elementName =
|
||||
value.tag.kind === 'BuiltinTag'
|
||||
? value.tag.name
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'this is checked as a place above',
|
||||
},
|
||||
],
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { input: t1 } = t0;
|
||||
const input = t1 === undefined ? "empty" : t1;
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[0] !== input) {
|
||||
t2 = () => {
|
||||
setCurrInput(input + "local const");
|
||||
};
|
||||
t3 = [input, "local const"];
|
||||
$[0] = input;
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== currInput) {
|
||||
t4 = <div>{currInput}</div>;
|
||||
$[3] = currInput;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ input: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>testlocal const</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { shouldChange } = t0;
|
||||
const [count, setCount] = useState(0);
|
||||
let t1;
|
||||
if ($[0] !== count || $[1] !== shouldChange) {
|
||||
t1 = () => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
};
|
||||
$[0] = count;
|
||||
$[1] = shouldChange;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== count) {
|
||||
t2 = [count];
|
||||
$[3] = count;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== count) {
|
||||
t3 = <div>{count}</div>;
|
||||
$[5] = count;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,15 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { firstName } = t0;
|
||||
const [lastName, setLastName] = useState("Doe");
|
||||
const [fullName, setFullName] = useState("John");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== firstName || $[1] !== lastName) {
|
||||
t1 = () => {
|
||||
setFullName(firstName + " " + "D." + " " + lastName);
|
||||
};
|
||||
t2 = [firstName, "D.", lastName];
|
||||
$[0] = firstName;
|
||||
$[1] = lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setLastName(e.target.value);
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== lastName) {
|
||||
t4 = <input value={lastName} onChange={t3} />;
|
||||
$[5] = lastName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== fullName) {
|
||||
t5 = <div>{fullName}</div>;
|
||||
$[7] = fullName;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== t4 || $[10] !== t5) {
|
||||
t6 = (
|
||||
<div>
|
||||
{t4}
|
||||
{t5}
|
||||
</div>
|
||||
);
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function localFunction() {
|
||||
console.log("local function");
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const localFunction = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== propValue) {
|
||||
t2 = () => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
};
|
||||
t3 = [propValue];
|
||||
$[1] = propValue;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== value) {
|
||||
t4 = <div>{value}</div>;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['local function']
|
||||
@@ -1,22 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-setter-call-outside-effect-no-error.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setName(initialName);
|
||||
| ^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [initialName]);
|
||||
10 |
|
||||
11 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-setter-used-outside-effect-no-error.ts:11:4
|
||||
9 | const [value, setValue] = useState(null);
|
||||
10 | useEffect(() => {
|
||||
> 11 | setValue(propValue);
|
||||
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [propValue]);
|
||||
13 |
|
||||
14 | return <MockComponent onSet={setValue} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.effect-with-global-function-call-no-error.ts:7:4
|
||||
5 | const [value, setValue] = useState(null);
|
||||
6 | useEffect(() => {
|
||||
> 7 | setValue(propValue);
|
||||
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
8 | globalCall();
|
||||
9 | }, [propValue]);
|
||||
10 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(5);
|
||||
const [firstName] = useState("Taylor");
|
||||
|
||||
const [fullName, setFullName] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== firstName) {
|
||||
t0 = () => {
|
||||
setFullName(firstName + " " + "Swift");
|
||||
};
|
||||
t1 = [firstName, "Swift"];
|
||||
$[0] = firstName;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== fullName) {
|
||||
t2 = <div>{fullName}</div>;
|
||||
$[3] = fullName;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Taylor Swift</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { props } = t0;
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + " " + props.lastName,
|
||||
);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== props.firstName || $[1] !== props.lastName) {
|
||||
t1 = () => {
|
||||
setFullName(props.firstName + " " + props.lastName);
|
||||
};
|
||||
t2 = [props.firstName, props.lastName];
|
||||
$[0] = props.firstName;
|
||||
$[1] = props.lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== fullName) {
|
||||
t3 = <div>{fullName}</div>;
|
||||
$[4] = fullName;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ props: { firstName: "John", lastName: "Doe" } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>John Doe</div>
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -12,7 +10,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
@@ -28,14 +26,14 @@ Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -8,7 +6,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
```javascript
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -30,6 +34,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
@@ -63,7 +68,7 @@ export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { identity, Stringify, useIdentity } from "shared-runtime";
|
||||
import * as SharedRuntime from "shared-runtime";
|
||||
|
||||
@@ -75,7 +80,12 @@ function Component(props) {
|
||||
const inner = { "Component[named > inner]": () => props.named }[
|
||||
"Component[named > inner]"
|
||||
];
|
||||
return inner();
|
||||
const innerIdentity = identity(
|
||||
{ "Component[named > identity()]": () => props.named }[
|
||||
"Component[named > identity()]"
|
||||
],
|
||||
);
|
||||
return inner(innerIdentity());
|
||||
};
|
||||
$[0] = props.named;
|
||||
$[1] = t0;
|
||||
@@ -83,6 +93,8 @@ function Component(props) {
|
||||
t0 = $[1];
|
||||
}
|
||||
const named = t0;
|
||||
|
||||
const callback = _ComponentCallback;
|
||||
let t1;
|
||||
if ($[2] !== props.namedVariable) {
|
||||
t1 = {
|
||||
@@ -197,57 +209,62 @@ function Component(props) {
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
const t10 = callback();
|
||||
let t11;
|
||||
if ($[19] !== namedVariable) {
|
||||
t10 = namedVariable();
|
||||
t11 = namedVariable();
|
||||
$[19] = namedVariable;
|
||||
$[20] = t10;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
const t11 = methodCall();
|
||||
const t12 = call();
|
||||
let t13;
|
||||
if ($[21] !== hookArgument) {
|
||||
t13 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t13;
|
||||
} else {
|
||||
t13 = $[22];
|
||||
t11 = $[20];
|
||||
}
|
||||
const t12 = methodCall();
|
||||
const t13 = call();
|
||||
let t14;
|
||||
if ($[21] !== hookArgument) {
|
||||
t14 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t14;
|
||||
} else {
|
||||
t14 = $[22];
|
||||
}
|
||||
let t15;
|
||||
if (
|
||||
$[23] !== builtinElementAttr ||
|
||||
$[24] !== namedElementAttr ||
|
||||
$[25] !== t10 ||
|
||||
$[26] !== t11 ||
|
||||
$[27] !== t12 ||
|
||||
$[28] !== t13 ||
|
||||
$[25] !== t11 ||
|
||||
$[26] !== t12 ||
|
||||
$[27] !== t13 ||
|
||||
$[28] !== t14 ||
|
||||
$[29] !== t9
|
||||
) {
|
||||
t14 = (
|
||||
t15 = (
|
||||
<>
|
||||
{t9}
|
||||
{t10}
|
||||
{t11}
|
||||
{t12}
|
||||
{t13}
|
||||
{builtinElementAttr}
|
||||
{namedElementAttr}
|
||||
{t13}
|
||||
{t14}
|
||||
</>
|
||||
);
|
||||
$[23] = builtinElementAttr;
|
||||
$[24] = namedElementAttr;
|
||||
$[25] = t10;
|
||||
$[26] = t11;
|
||||
$[27] = t12;
|
||||
$[28] = t13;
|
||||
$[25] = t11;
|
||||
$[26] = t12;
|
||||
$[27] = t13;
|
||||
$[28] = t14;
|
||||
$[29] = t9;
|
||||
$[30] = t14;
|
||||
$[30] = t15;
|
||||
} else {
|
||||
t14 = $[30];
|
||||
t15 = $[30];
|
||||
}
|
||||
return t14;
|
||||
return t15;
|
||||
}
|
||||
function _ComponentCallback() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -26,6 +30,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @script
|
||||
const React = require('react');
|
||||
|
||||
function Component(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{name: 'React Compiler'}],
|
||||
},
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
const { c: _c } = require("react/compiler-runtime"); // @script
|
||||
const React = require("react");
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = <div>{props.name}</div>;
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{ name: "React Compiler" }],
|
||||
},
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>React Compiler</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
// @script
|
||||
const React = require('react');
|
||||
|
||||
function Component(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{name: 'React Compiler'}],
|
||||
},
|
||||
};
|
||||
@@ -31,10 +31,15 @@ import prettier from 'prettier';
|
||||
import SproutTodoFilter from './SproutTodoFilter';
|
||||
import {isExpectError} from './fixture-utils';
|
||||
import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';
|
||||
|
||||
export function parseLanguage(source: string): 'flow' | 'typescript' {
|
||||
return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
|
||||
}
|
||||
|
||||
export function parseSourceType(source: string): 'script' | 'module' {
|
||||
return source.indexOf('@script') !== -1 ? 'script' : 'module';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse react compiler plugin + environment options from test fixture. Note
|
||||
* that although this primarily uses `Environment:parseConfigPragma`, it also
|
||||
@@ -98,6 +103,7 @@ export function parseInput(
|
||||
input: string,
|
||||
filename: string,
|
||||
language: 'flow' | 'typescript',
|
||||
sourceType: 'module' | 'script',
|
||||
): BabelCore.types.File {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
@@ -105,14 +111,14 @@ export function parseInput(
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceFilename: filename,
|
||||
sourceType: 'module',
|
||||
sourceType,
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return BabelParser.parse(input, {
|
||||
sourceFilename: filename,
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
sourceType,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -221,11 +227,12 @@ export async function transformFixtureInput(
|
||||
const firstLine = input.substring(0, input.indexOf('\n'));
|
||||
|
||||
const language = parseLanguage(firstLine);
|
||||
const sourceType = parseSourceType(firstLine);
|
||||
// Preserve file extension as it determines typescript's babel transform
|
||||
// mode (e.g. stripping types, parsing rules for brackets)
|
||||
const filename =
|
||||
path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');
|
||||
const inputAst = parseInput(input, filename, language);
|
||||
const inputAst = parseInput(input, filename, language, sourceType);
|
||||
// Give babel transforms an absolute path as relative paths get prefixed
|
||||
// with `cwd`, which is different across machines
|
||||
const virtualFilepath = '/' + filename;
|
||||
|
||||
@@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult {
|
||||
return {
|
||||
kind: 'UnexpectedError',
|
||||
value:
|
||||
'Unexpected error during eval, possible syntax error?\n' + e.message,
|
||||
'Unexpected error during eval, possible syntax error?\n' +
|
||||
e.message +
|
||||
'\n\nsource:\n' +
|
||||
source,
|
||||
logs,
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -1430,6 +1430,72 @@ if (__EXPERIMENTAL__) {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
|
||||
// and useInsertionEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
allTests.invalid = [
|
||||
...allTests.invalid,
|
||||
|
||||
@@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace(
|
||||
return node;
|
||||
}
|
||||
|
||||
function isUseEffectIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && node.name === 'useEffect';
|
||||
function isEffectIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
|
||||
}
|
||||
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
if (__EXPERIMENTAL__) {
|
||||
@@ -726,7 +726,7 @@ const rule = {
|
||||
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
|
||||
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
|
||||
if (
|
||||
(isUseEffectIdentifier(nodeWithoutNamespace) ||
|
||||
(isEffectIdentifier(nodeWithoutNamespace) ||
|
||||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@jridgewell/sourcemap-codec": "1.5.5",
|
||||
"acorn-jsx": "^5.2.0",
|
||||
"archiver": "^3.0.0",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
@@ -60,7 +61,6 @@
|
||||
"raw-loader": "^3.1.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-js": "^0.6.2",
|
||||
"sourcemap-codec": "^1.4.8",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
|
||||
@@ -23,7 +23,7 @@ function fetchResource(url) {
|
||||
});
|
||||
};
|
||||
|
||||
fetch(url, {cache: 'force-cache'}).then(
|
||||
fetch(url, {cache: 'force-cache', signal: AbortSignal.timeout(60000)}).then(
|
||||
response => {
|
||||
if (response.ok) {
|
||||
response
|
||||
|
||||
@@ -78,6 +78,18 @@ const fetchFromNetworkCache = (url, resolve, reject) => {
|
||||
});
|
||||
};
|
||||
|
||||
const pendingFetchRequests = new Set();
|
||||
function pendingFetchRequestsCleanup({payload, source}) {
|
||||
if (source === 'react-devtools-background') {
|
||||
switch (payload?.type) {
|
||||
case 'fetch-file-with-cache-complete':
|
||||
case 'fetch-file-with-cache-error':
|
||||
pendingFetchRequests.delete(payload.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
chrome.runtime.onMessage.addListener(pendingFetchRequestsCleanup);
|
||||
|
||||
const fetchFromPage = async (url, resolve, reject) => {
|
||||
debugLog('[main] fetchFromPage()', url);
|
||||
|
||||
@@ -97,7 +109,11 @@ const fetchFromPage = async (url, resolve, reject) => {
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(onPortMessage);
|
||||
if (pendingFetchRequests.has(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingFetchRequests.add(url);
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'devtools-page',
|
||||
payload: {
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"test:e2e": "playwright test --config=playwright.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map-js": "^0.6.2",
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
"@jridgewell/sourcemap-codec": "1.5.5",
|
||||
"source-map-js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
|
||||
@@ -130,24 +130,28 @@ describe('Timeline profiler', () => {
|
||||
// @reactVersion <= 18.2
|
||||
// @reactVersion >= 18.0
|
||||
it('should mark sync render without suspends or state updates', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(<div />);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
// TODO(hoxyq): investigate why running this test with React 18 fails
|
||||
@@ -260,46 +264,50 @@ describe('Timeline profiler', () => {
|
||||
throw Error('Expected error');
|
||||
}
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(
|
||||
<ErrorBoundary>
|
||||
<ExampleThatThrows />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1095,24 +1103,28 @@ describe('Timeline profiler', () => {
|
||||
// @reactVersion <= 18.2
|
||||
// @reactVersion >= 18.0
|
||||
it('regression test SyncLane', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(<div />);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1432,19 +1444,19 @@ describe('Timeline profiler', () => {
|
||||
expect(timelineData.suspenseEvents).toHaveLength(1);
|
||||
const suspenseEvent = timelineData.suspenseEvents[0];
|
||||
expect(suspenseEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 10,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "resolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 0,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "unresolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
|
||||
// There should be two batches of renders: Suspeneded and resolved.
|
||||
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
|
||||
@@ -1490,19 +1502,19 @@ describe('Timeline profiler', () => {
|
||||
expect(timelineData.suspenseEvents).toHaveLength(1);
|
||||
const suspenseEvent = timelineData.suspenseEvents[0];
|
||||
expect(suspenseEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 10,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "rejected",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 0,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "unresolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
|
||||
// There should be two batches of renders: Suspeneded and resolved.
|
||||
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
|
||||
|
||||
@@ -111,9 +111,31 @@ describe('Timeline profiler', () => {
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
waitFor = InternalTestUtils.waitFor;
|
||||
if (typeof Scheduler.log !== 'function') {
|
||||
// backwards compat for older scheduler versions
|
||||
Scheduler.log = Scheduler.unstable_yieldValue;
|
||||
Scheduler.unstable_clearLog = Scheduler.unstable_clearYields;
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
|
||||
// polyfill waitFor as Scheduler.toFlushAndYieldThrough
|
||||
waitFor = expectedYields => {
|
||||
let actualYields = Scheduler.unstable_clearYields();
|
||||
if (actualYields.length !== 0) {
|
||||
throw new Error(
|
||||
'Log of yielded values is not empty. ' +
|
||||
'Call expect(Scheduler).toHaveYielded(...) first.',
|
||||
);
|
||||
}
|
||||
Scheduler.unstable_flushNumberOfYields(expectedYields.length);
|
||||
actualYields = Scheduler.unstable_clearYields();
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
};
|
||||
} else {
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
waitFor = InternalTestUtils.waitFor;
|
||||
}
|
||||
|
||||
setPerformanceMock =
|
||||
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;
|
||||
|
||||
@@ -725,14 +725,14 @@ describe('ProfilingCache', () => {
|
||||
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
|
||||
expect(commitData).toHaveLength(2);
|
||||
|
||||
const isLegacySuspense = React.version.startsWith('17');
|
||||
if (isLegacySuspense) {
|
||||
if (React.version.startsWith('17')) {
|
||||
// React 17 will mount all children until it suspends in a LegacyHidden
|
||||
// The ID gap is from the Fiber for <Async> that's in the disconnected tree.
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
@@ -741,7 +741,6 @@ describe('ProfilingCache', () => {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -19,8 +19,6 @@ describe('commit tree', () => {
|
||||
let Scheduler;
|
||||
let store: Store;
|
||||
let utils;
|
||||
const isLegacySuspense =
|
||||
React.version.startsWith('16') || React.version.startsWith('17');
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
@@ -186,24 +184,13 @@ describe('commit tree', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
await Promise.resolve();
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -231,13 +218,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3);
|
||||
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
|
||||
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
@@ -291,24 +272,13 @@ describe('commit tree', () => {
|
||||
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -327,13 +297,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3);
|
||||
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
|
||||
|
||||
@@ -2828,7 +2828,7 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion >= 17.0
|
||||
it('can reconcile Suspense in fallback positions', async () => {
|
||||
let resolveFallback;
|
||||
const fallbackPromise = new Promise(resolve => {
|
||||
@@ -2907,7 +2907,7 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion >= 17.0
|
||||
it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => {
|
||||
let resolveHeadFallback;
|
||||
let resolveHeadContent;
|
||||
|
||||
@@ -460,10 +460,10 @@ export function getInternalReactConstants(version: string): {
|
||||
IncompleteFunctionComponent: 28,
|
||||
IndeterminateComponent: 2, // removed in 19.0.0
|
||||
LazyComponent: 16,
|
||||
LegacyHiddenComponent: 23,
|
||||
LegacyHiddenComponent: 23, // Does not exist in 18+ OSS but exists in fb builds
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
OffscreenComponent: 22, // Experimental
|
||||
OffscreenComponent: 22, // Experimental in 17. Stable in 18+
|
||||
Profiler: 12,
|
||||
ScopeComponent: 21, // Experimental
|
||||
SimpleMemoComponent: 15,
|
||||
@@ -3057,13 +3057,23 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function isHiddenOffscreen(fiber: Fiber): boolean {
|
||||
switch (fiber.tag) {
|
||||
case LegacyHiddenComponent:
|
||||
// fallthrough since all published implementations currently implement the same state as Offscreen.
|
||||
case OffscreenComponent:
|
||||
return fiber.memoizedState !== null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function unmountRemainingChildren() {
|
||||
if (
|
||||
reconcilingParent !== null &&
|
||||
(reconcilingParent.kind === FIBER_INSTANCE ||
|
||||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
reconcilingParent.data.tag === OffscreenComponent &&
|
||||
reconcilingParent.data.memoizedState !== null &&
|
||||
isHiddenOffscreen(reconcilingParent.data) &&
|
||||
!isInDisconnectedSubtree
|
||||
) {
|
||||
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
|
||||
@@ -3170,8 +3180,7 @@ export function attach(
|
||||
if (
|
||||
(parent.kind === FIBER_INSTANCE ||
|
||||
parent.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
parent.data.tag === OffscreenComponent &&
|
||||
parent.data.memoizedState !== null
|
||||
isHiddenOffscreen(parent.data)
|
||||
) {
|
||||
// We're inside a hidden offscreen Fiber. We're in a disconnected tree.
|
||||
return;
|
||||
@@ -3819,7 +3828,9 @@ export function attach(
|
||||
(reconcilingParent !== null &&
|
||||
reconcilingParent.kind === VIRTUAL_INSTANCE) ||
|
||||
fiber.tag === SuspenseComponent ||
|
||||
fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent.
|
||||
// Use to keep resuspended instances alive inside a SuspenseComponent.
|
||||
fiber.tag === OffscreenComponent ||
|
||||
fiber.tag === LegacyHiddenComponent
|
||||
) {
|
||||
// If the parent is a Virtual Instance and we filtered this Fiber we include a
|
||||
// hidden node. We also include this if it's a Suspense boundary so we can track those
|
||||
@@ -3939,7 +3950,7 @@ export function attach(
|
||||
trackDebugInfoFromHostComponent(nearestInstance, fiber);
|
||||
}
|
||||
|
||||
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
|
||||
if (isHiddenOffscreen(fiber)) {
|
||||
// If an Offscreen component is hidden, mount its children as disconnected.
|
||||
const stashedDisconnected = isInDisconnectedSubtree;
|
||||
isInDisconnectedSubtree = true;
|
||||
@@ -4261,7 +4272,7 @@ export function attach(
|
||||
while (child !== null) {
|
||||
if (child.kind === FILTERED_FIBER_INSTANCE) {
|
||||
const fiber = child.data;
|
||||
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
|
||||
if (isHiddenOffscreen(fiber)) {
|
||||
// The children of this Offscreen are hidden so they don't get added.
|
||||
} else {
|
||||
addUnfilteredChildrenIDs(child, nextChildren);
|
||||
@@ -4888,9 +4899,8 @@ export function attach(
|
||||
const nextDidTimeOut =
|
||||
isLegacySuspense && nextFiber.memoizedState !== null;
|
||||
|
||||
const isOffscreen = nextFiber.tag === OffscreenComponent;
|
||||
const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null;
|
||||
const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null;
|
||||
const prevWasHidden = isHiddenOffscreen(prevFiber);
|
||||
const nextIsHidden = isHiddenOffscreen(nextFiber);
|
||||
|
||||
if (isLegacySuspense) {
|
||||
if (
|
||||
@@ -5245,8 +5255,7 @@ export function attach(
|
||||
if (
|
||||
(child.kind === FIBER_INSTANCE ||
|
||||
child.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
child.data.tag === OffscreenComponent &&
|
||||
child.data.memoizedState !== null
|
||||
isHiddenOffscreen(child.data)
|
||||
) {
|
||||
// This instance's children are already disconnected.
|
||||
} else {
|
||||
@@ -5275,8 +5284,7 @@ export function attach(
|
||||
if (
|
||||
(child.kind === FIBER_INSTANCE ||
|
||||
child.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
child.data.tag === OffscreenComponent &&
|
||||
child.data.memoizedState !== null
|
||||
isHiddenOffscreen(child.data)
|
||||
) {
|
||||
// This instance's children should remain disconnected.
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
|
||||
import {decode} from 'sourcemap-codec';
|
||||
import {decode} from '@jridgewell/sourcemap-codec';
|
||||
|
||||
import type {
|
||||
IndexSourceMap,
|
||||
@@ -47,7 +47,7 @@ export default function SourceMapConsumer(
|
||||
|
||||
function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
|
||||
const decodedMappings: Mappings = withSyncPerfMeasurements(
|
||||
'Decoding source map mappings with sourcemap-codec',
|
||||
'Decoding source map mappings with @jridgewell/sourcemap-codec',
|
||||
() => decode(sourceMapJSON.mappings),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const babel = require('@rollup/plugin-babel').babel;
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const jsx = require('acorn-jsx');
|
||||
const rollupResolve = require('@rollup/plugin-node-resolve').nodeResolve;
|
||||
const {encode, decode} = require('sourcemap-codec');
|
||||
const {encode, decode} = require('@jridgewell/sourcemap-codec');
|
||||
const {generateEncodedHookMap} = require('../generateHookMap');
|
||||
const {parse} = require('@babel/parser');
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {getHookNamesMappingFromAST} from './astUtils';
|
||||
import {encode, decode} from 'sourcemap-codec';
|
||||
import {encode, decode} from '@jridgewell/sourcemap-codec';
|
||||
|
||||
// Missing types in @babel/types
|
||||
type File = any;
|
||||
|
||||
@@ -2320,6 +2320,9 @@ export function startViewTransition(
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
@@ -2509,6 +2512,7 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
@@ -2723,6 +2727,12 @@ export function startGestureTransition(
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.__reactViewTransition = null;
|
||||
}
|
||||
if (enableProfilerTimer) {
|
||||
// Signal that the Transition was unable to continue. We do that here
|
||||
// instead of when we stop the running View Transition to ensure that
|
||||
// we cover cases when something else stops it early.
|
||||
finishedAnimation();
|
||||
}
|
||||
});
|
||||
return transition;
|
||||
} catch (x) {
|
||||
@@ -2735,6 +2745,9 @@ export function startGestureTransition(
|
||||
// Run through the sequence to put state back into a consistent state.
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1613,6 +1613,116 @@ describe('FragmentRefs', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('compares a root-level Fragment', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const emptyFragmentRef = React.createRef();
|
||||
const childRef = React.createRef();
|
||||
const siblingPrecedingRef = React.createRef();
|
||||
const siblingFollowingRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div ref={siblingPrecedingRef} />
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div ref={childRef} />
|
||||
</Fragment>
|
||||
<Fragment ref={emptyFragmentRef} />
|
||||
<div ref={siblingFollowingRef} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => root.render(<Test />));
|
||||
|
||||
const fragmentInstance = fragmentRef.current;
|
||||
if (fragmentInstance == null) {
|
||||
throw new Error('Expected fragment instance to be non-null');
|
||||
}
|
||||
const emptyFragmentInstance = emptyFragmentRef.current;
|
||||
if (emptyFragmentInstance == null) {
|
||||
throw new Error('Expected empty fragment instance to be non-null');
|
||||
}
|
||||
|
||||
expectPosition(
|
||||
fragmentInstance.compareDocumentPosition(childRef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: true,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
fragmentInstance.compareDocumentPosition(siblingPrecedingRef.current),
|
||||
{
|
||||
preceding: true,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
fragmentInstance.compareDocumentPosition(siblingFollowingRef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: true,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
emptyFragmentInstance.compareDocumentPosition(childRef.current),
|
||||
{
|
||||
preceding: true,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: true,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
emptyFragmentInstance.compareDocumentPosition(
|
||||
siblingPrecedingRef.current,
|
||||
),
|
||||
{
|
||||
preceding: true,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: true,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
emptyFragmentInstance.compareDocumentPosition(
|
||||
siblingFollowingRef.current,
|
||||
),
|
||||
{
|
||||
preceding: false,
|
||||
following: true,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with portals', () => {
|
||||
// @gate enableFragmentRefs
|
||||
it('handles portaled elements', async () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import {HostText} from 'react-reconciler/src/ReactWorkTags';
|
||||
import {
|
||||
getFragmentParentHostFiber,
|
||||
getInstanceFromHostFiber,
|
||||
traverseFragmentInstance,
|
||||
} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
|
||||
@@ -303,6 +302,13 @@ export function getPublicInstance(instance: Instance): null | PublicInstance {
|
||||
return instance.canonical.publicInstance;
|
||||
}
|
||||
|
||||
// Handle root containers
|
||||
if (instance.containerInfo != null) {
|
||||
if (instance.containerInfo.publicInstance != null) {
|
||||
return instance.containerInfo.publicInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// For compatibility with the legacy renderer, in case it's used with Fabric
|
||||
// in the same app.
|
||||
// $FlowExpectedError[prop-missing]
|
||||
@@ -347,9 +353,8 @@ export function getPublicInstanceFromInternalInstanceHandle(
|
||||
}
|
||||
|
||||
function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance {
|
||||
const instance = getInstanceFromHostFiber<Instance>(fiber);
|
||||
const publicInstance = getPublicInstance(instance);
|
||||
if (publicInstance == null) {
|
||||
const publicInstance = getPublicInstance(fiber.stateNode);
|
||||
if (publicInstance === null) {
|
||||
throw new Error('Expected to find a host node. This is a bug in React.');
|
||||
}
|
||||
return publicInstance;
|
||||
@@ -698,11 +703,11 @@ FragmentInstance.prototype.compareDocumentPosition = function (
|
||||
if (parentHostFiber === null) {
|
||||
return Node.DOCUMENT_POSITION_DISCONNECTED;
|
||||
}
|
||||
const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
|
||||
const children: Array<Fiber> = [];
|
||||
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
|
||||
if (children.length === 0) {
|
||||
return compareDocumentPositionForEmptyFragment(
|
||||
const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
|
||||
return compareDocumentPositionForEmptyFragment<PublicInstance>(
|
||||
this._fragmentFiber,
|
||||
parentHostInstance,
|
||||
otherNode,
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
} from 'react-reconciler/src/ReactEventPriorities';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
|
||||
@@ -680,6 +682,9 @@ export function startViewTransition(
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
@@ -696,9 +701,13 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,12 @@ import {
|
||||
getViewTransitionClassName,
|
||||
} from './ReactFiberViewTransitionComponent';
|
||||
|
||||
import {
|
||||
enableProfilerTimer,
|
||||
enableComponentPerformanceTrack,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {trackAnimatingTask} from './ReactProfilerTimer';
|
||||
|
||||
let didWarnForRootClone = false;
|
||||
|
||||
// Used during the apply phase to track whether a parent ViewTransition component
|
||||
@@ -101,6 +107,7 @@ function applyViewTransitionToClones(
|
||||
name: string,
|
||||
className: ?string,
|
||||
clones: Array<Instance>,
|
||||
fiber: Fiber,
|
||||
): void {
|
||||
// This gets called when we have found a pair, but after the clone in created. The clone is
|
||||
// created by the insertion side. If the insertion side if found before the deletion side
|
||||
@@ -117,6 +124,11 @@ function applyViewTransitionToClones(
|
||||
className,
|
||||
);
|
||||
}
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (fiber._debugTask != null) {
|
||||
trackAnimatingTask(fiber._debugTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trackDeletedPairViewTransitions(deletion: Fiber): void {
|
||||
@@ -171,7 +183,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
|
||||
// If we have clones that means that we've already visited this
|
||||
// ViewTransition boundary before and we can now apply the name
|
||||
// to those clones. Otherwise, we have to wait until we clone it.
|
||||
applyViewTransitionToClones(name, className, clones);
|
||||
applyViewTransitionToClones(name, className, clones, child);
|
||||
}
|
||||
}
|
||||
if (pairs.size === 0) {
|
||||
@@ -221,7 +233,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
|
||||
// If we have clones that means that we've already visited this
|
||||
// ViewTransition boundary before and we can now apply the name
|
||||
// to those clones. Otherwise, we have to wait until we clone it.
|
||||
applyViewTransitionToClones(name, className, clones);
|
||||
applyViewTransitionToClones(name, className, clones, deletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,7 +278,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
|
||||
// If there are no clones at this point, that should mean that there are no
|
||||
// HostComponent children in this ViewTransition.
|
||||
if (clones !== null) {
|
||||
applyViewTransitionToClones(name, className, clones);
|
||||
applyViewTransitionToClones(name, className, clones, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +308,7 @@ function applyExitViewTransition(placement: Fiber): void {
|
||||
// If there are no clones at this point, that should mean that there are no
|
||||
// HostComponent children in this ViewTransition.
|
||||
if (clones !== null) {
|
||||
applyViewTransitionToClones(name, className, clones);
|
||||
applyViewTransitionToClones(name, className, clones, placement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +326,7 @@ function applyNestedViewTransition(child: Fiber): void {
|
||||
// If there are no clones at this point, that should mean that there are no
|
||||
// HostComponent children in this ViewTransition.
|
||||
if (clones !== null) {
|
||||
applyViewTransitionToClones(name, className, clones);
|
||||
applyViewTransitionToClones(name, className, clones, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +358,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
|
||||
// If there are no clones at this point, that should mean that there are no
|
||||
// HostComponent children in this ViewTransition.
|
||||
if (clones !== null) {
|
||||
applyViewTransitionToClones(oldName, className, clones);
|
||||
applyViewTransitionToClones(oldName, className, clones, finishedWork);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
packages/react-reconciler/src/ReactFiberLane.js
vendored
21
packages/react-reconciler/src/ReactFiberLane.js
vendored
@@ -28,6 +28,7 @@ import {
|
||||
retryLaneExpirationMs,
|
||||
disableLegacyMode,
|
||||
enableDefaultTransitionIndicator,
|
||||
enableGestureTransition,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
|
||||
import {clz32} from './clz32';
|
||||
@@ -611,10 +612,6 @@ export function includesSyncLane(lanes: Lanes): boolean {
|
||||
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
|
||||
}
|
||||
|
||||
export function isSyncLane(lanes: Lanes): boolean {
|
||||
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
|
||||
}
|
||||
|
||||
export function includesNonIdleWork(lanes: Lanes): boolean {
|
||||
return (lanes & NonIdleLanes) !== NoLanes;
|
||||
}
|
||||
@@ -681,6 +678,8 @@ export function includesLoadingIndicatorLanes(lanes: Lanes): boolean {
|
||||
|
||||
export function includesBlockingLane(lanes: Lanes): boolean {
|
||||
const SyncDefaultLanes =
|
||||
SyncHydrationLane |
|
||||
SyncLane |
|
||||
InputContinuousHydrationLane |
|
||||
InputContinuousLane |
|
||||
DefaultHydrationLane |
|
||||
@@ -697,10 +696,13 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
|
||||
|
||||
export function isBlockingLane(lane: Lane): boolean {
|
||||
const SyncDefaultLanes =
|
||||
SyncHydrationLane |
|
||||
SyncLane |
|
||||
InputContinuousHydrationLane |
|
||||
InputContinuousLane |
|
||||
DefaultHydrationLane |
|
||||
DefaultLane;
|
||||
DefaultLane |
|
||||
GestureLane;
|
||||
return (lane & SyncDefaultLanes) !== NoLanes;
|
||||
}
|
||||
|
||||
@@ -709,6 +711,9 @@ export function isTransitionLane(lane: Lane): boolean {
|
||||
}
|
||||
|
||||
export function isGestureRender(lanes: Lanes): boolean {
|
||||
if (!enableGestureTransition) {
|
||||
return false;
|
||||
}
|
||||
// This should render only the one lane.
|
||||
return lanes === GestureLane;
|
||||
}
|
||||
@@ -1270,11 +1275,13 @@ export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
|
||||
InputContinuousHydrationLane |
|
||||
InputContinuousLane |
|
||||
DefaultHydrationLane |
|
||||
DefaultLane |
|
||||
GestureLane)
|
||||
DefaultLane)
|
||||
) {
|
||||
return 'Blocking';
|
||||
}
|
||||
if (lanes & GestureLane) {
|
||||
return 'Gesture';
|
||||
}
|
||||
if (lanes & (TransitionHydrationLane | TransitionLanes)) {
|
||||
return 'Transition';
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
addObjectDiffToProperties,
|
||||
} from 'shared/ReactPerformanceTrackProperties';
|
||||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableProfilerTimer,
|
||||
enableGestureTransition,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
@@ -68,6 +71,16 @@ export function markAllLanesInOrder() {
|
||||
LANES_TRACK_GROUP,
|
||||
'primary-light',
|
||||
);
|
||||
if (enableGestureTransition) {
|
||||
console.timeStamp(
|
||||
'Gesture Track',
|
||||
0.003,
|
||||
0.003,
|
||||
'Gesture',
|
||||
LANES_TRACK_GROUP,
|
||||
'primary-light',
|
||||
);
|
||||
}
|
||||
console.timeStamp(
|
||||
'Transition Track',
|
||||
0.003,
|
||||
@@ -739,6 +752,111 @@ export function logBlockingStart(
|
||||
}
|
||||
}
|
||||
|
||||
export function logGestureStart(
|
||||
updateTime: number,
|
||||
eventTime: number,
|
||||
eventType: null | string,
|
||||
eventIsRepeat: boolean,
|
||||
isPingedUpdate: boolean,
|
||||
renderStartTime: number,
|
||||
debugTask: null | ConsoleTask, // DEV-only
|
||||
updateMethodName: null | string,
|
||||
updateComponentName: null | string,
|
||||
): void {
|
||||
if (supportsUserTiming) {
|
||||
currentTrack = 'Gesture';
|
||||
// Clamp start times
|
||||
if (updateTime > 0) {
|
||||
if (updateTime > renderStartTime) {
|
||||
updateTime = renderStartTime;
|
||||
}
|
||||
} else {
|
||||
updateTime = renderStartTime;
|
||||
}
|
||||
if (eventTime > 0) {
|
||||
if (eventTime > updateTime) {
|
||||
eventTime = updateTime;
|
||||
}
|
||||
} else {
|
||||
eventTime = updateTime;
|
||||
}
|
||||
|
||||
if (updateTime > eventTime && eventType !== null) {
|
||||
// Log the time from the event timeStamp until we started a gesture.
|
||||
const color = eventIsRepeat ? 'secondary-light' : 'warning';
|
||||
if (__DEV__ && debugTask) {
|
||||
debugTask.run(
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
|
||||
eventTime,
|
||||
updateTime,
|
||||
currentTrack,
|
||||
LANES_TRACK_GROUP,
|
||||
color,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
|
||||
eventTime,
|
||||
updateTime,
|
||||
currentTrack,
|
||||
LANES_TRACK_GROUP,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (renderStartTime > updateTime) {
|
||||
// Log the time from when we called setState until we started rendering.
|
||||
const label = isPingedUpdate
|
||||
? 'Promise Resolved'
|
||||
: renderStartTime - updateTime > 5
|
||||
? 'Gesture Blocked'
|
||||
: 'Gesture';
|
||||
if (__DEV__) {
|
||||
const properties = [];
|
||||
if (updateComponentName != null) {
|
||||
properties.push(['Component name', updateComponentName]);
|
||||
}
|
||||
if (updateMethodName != null) {
|
||||
properties.push(['Method name', updateMethodName]);
|
||||
}
|
||||
const measureOptions = {
|
||||
start: updateTime,
|
||||
end: renderStartTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
properties,
|
||||
track: currentTrack,
|
||||
trackGroup: LANES_TRACK_GROUP,
|
||||
color: 'primary-light',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (debugTask) {
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, label, measureOptions),
|
||||
);
|
||||
} else {
|
||||
performance.measure(label, measureOptions);
|
||||
}
|
||||
} else {
|
||||
console.timeStamp(
|
||||
label,
|
||||
updateTime,
|
||||
renderStartTime,
|
||||
currentTrack,
|
||||
LANES_TRACK_GROUP,
|
||||
'primary-light',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logTransitionStart(
|
||||
startTime: number,
|
||||
updateTime: number,
|
||||
|
||||
114
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
114
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
@@ -71,6 +71,7 @@ import {
|
||||
} from './Scheduler';
|
||||
import {
|
||||
logBlockingStart,
|
||||
logGestureStart,
|
||||
logTransitionStart,
|
||||
logRenderPhase,
|
||||
logInterruptedRenderPhase,
|
||||
@@ -282,6 +283,16 @@ import {
|
||||
blockingEventType,
|
||||
blockingEventIsRepeat,
|
||||
blockingSuspendedTime,
|
||||
gestureClampTime,
|
||||
gestureUpdateTime,
|
||||
gestureUpdateTask,
|
||||
gestureUpdateType,
|
||||
gestureUpdateMethodName,
|
||||
gestureUpdateComponentName,
|
||||
gestureEventTime,
|
||||
gestureEventType,
|
||||
gestureEventIsRepeat,
|
||||
gestureSuspendedTime,
|
||||
transitionClampTime,
|
||||
transitionStartTime,
|
||||
transitionUpdateTime,
|
||||
@@ -294,8 +305,11 @@ import {
|
||||
transitionEventIsRepeat,
|
||||
transitionSuspendedTime,
|
||||
clearBlockingTimers,
|
||||
clearGestureTimers,
|
||||
clearGestureUpdates,
|
||||
clearTransitionTimers,
|
||||
clampBlockingTimers,
|
||||
clampGestureTimers,
|
||||
clampTransitionTimers,
|
||||
clampRetryTimers,
|
||||
clampIdleTimers,
|
||||
@@ -1898,7 +1912,9 @@ function resetWorkInProgressStack() {
|
||||
|
||||
function finalizeRender(lanes: Lanes, finalizationTime: number): void {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
|
||||
if (isGestureRender(lanes)) {
|
||||
clampGestureTimers(finalizationTime);
|
||||
} else if (includesBlockingLane(lanes)) {
|
||||
clampBlockingTimers(finalizationTime);
|
||||
}
|
||||
if (includesTransitionLane(lanes)) {
|
||||
@@ -1963,7 +1979,53 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
|
||||
const previousUpdateTask = workInProgressUpdateTask;
|
||||
|
||||
workInProgressUpdateTask = null;
|
||||
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
|
||||
if (isGestureRender(lanes)) {
|
||||
workInProgressUpdateTask = gestureUpdateTask;
|
||||
const clampedUpdateTime =
|
||||
gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime
|
||||
? gestureClampTime
|
||||
: gestureUpdateTime;
|
||||
const clampedEventTime =
|
||||
gestureEventTime >= 0 && gestureEventTime < gestureClampTime
|
||||
? gestureClampTime
|
||||
: gestureEventTime;
|
||||
const clampedRenderStartTime =
|
||||
// Clamp the suspended time to the first event/update.
|
||||
clampedEventTime >= 0
|
||||
? clampedEventTime
|
||||
: clampedUpdateTime >= 0
|
||||
? clampedUpdateTime
|
||||
: renderStartTime;
|
||||
if (gestureSuspendedTime >= 0) {
|
||||
setCurrentTrackFromLanes(GestureLane);
|
||||
logSuspendedWithDelayPhase(
|
||||
gestureSuspendedTime,
|
||||
clampedRenderStartTime,
|
||||
lanes,
|
||||
workInProgressUpdateTask,
|
||||
);
|
||||
} else if (isGestureRender(animatingLanes)) {
|
||||
// If this lane is still animating, log the time from previous render finishing to now as animating.
|
||||
setCurrentTrackFromLanes(GestureLane);
|
||||
logAnimatingPhase(
|
||||
gestureClampTime,
|
||||
clampedRenderStartTime,
|
||||
animatingTask,
|
||||
);
|
||||
}
|
||||
logGestureStart(
|
||||
clampedUpdateTime,
|
||||
clampedEventTime,
|
||||
gestureEventType,
|
||||
gestureEventIsRepeat,
|
||||
gestureUpdateType === PINGED_UPDATE,
|
||||
renderStartTime,
|
||||
gestureUpdateTask,
|
||||
gestureUpdateMethodName,
|
||||
gestureUpdateComponentName,
|
||||
);
|
||||
clearGestureTimers();
|
||||
} else if (includesBlockingLane(lanes)) {
|
||||
workInProgressUpdateTask = blockingUpdateTask;
|
||||
const clampedUpdateTime =
|
||||
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
|
||||
@@ -1988,7 +2050,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
|
||||
previousUpdateTask,
|
||||
);
|
||||
} else if (
|
||||
includesSyncLane(animatingLanes) ||
|
||||
!isGestureRender(animatingLanes) &&
|
||||
includesBlockingLane(animatingLanes)
|
||||
) {
|
||||
// If this lane is still animating, log the time from previous render finishing to now as animating.
|
||||
@@ -3464,6 +3526,11 @@ function commitRoot(
|
||||
// Gestures don't clear their lanes while the gesture is still active but it
|
||||
// might not be scheduled to do any more renders and so we shouldn't schedule
|
||||
// any more gesture lane work until a new gesture is scheduled.
|
||||
if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) {
|
||||
// We need to clear any updates scheduled so that we can treat future updates
|
||||
// as the cause of the render.
|
||||
clearGestureUpdates();
|
||||
}
|
||||
remainingLanes &= ~GestureLane;
|
||||
}
|
||||
|
||||
@@ -3719,14 +3786,12 @@ function finishedViewTransition(lanes: Lanes): void {
|
||||
// If an affected track isn't in the middle of rendering or committing, log from the previous
|
||||
// finished render until the end of the animation.
|
||||
if (
|
||||
(includesSyncLane(lanes) || includesBlockingLane(lanes)) &&
|
||||
!includesSyncLane(workInProgressRootRenderLanes) &&
|
||||
!includesBlockingLane(workInProgressRootRenderLanes) &&
|
||||
!includesSyncLane(pendingEffectsLanes) &&
|
||||
!includesBlockingLane(pendingEffectsLanes)
|
||||
isGestureRender(lanes) &&
|
||||
!isGestureRender(workInProgressRootRenderLanes) &&
|
||||
!isGestureRender(pendingEffectsLanes)
|
||||
) {
|
||||
setCurrentTrackFromLanes(SyncLane);
|
||||
logAnimatingPhase(blockingClampTime, now(), task);
|
||||
setCurrentTrackFromLanes(GestureLane);
|
||||
logAnimatingPhase(gestureClampTime, now(), task);
|
||||
}
|
||||
if (
|
||||
includesTransitionLane(lanes) &&
|
||||
@@ -4189,6 +4254,10 @@ function commitGestureOnRoot(
|
||||
}
|
||||
deleteScheduledGesture(root, finishedGesture);
|
||||
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
startAnimating(pendingEffectsLanes);
|
||||
}
|
||||
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
ReactSharedInternals.T = null;
|
||||
const previousPriority = getCurrentUpdatePriority();
|
||||
@@ -4216,6 +4285,10 @@ function commitGestureOnRoot(
|
||||
flushGestureMutations,
|
||||
flushGestureAnimations,
|
||||
reportViewTransitionError,
|
||||
enableProfilerTimer
|
||||
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
|
||||
finishedViewTransition.bind(null, pendingEffectsLanes)
|
||||
: (null: any),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4258,6 +4331,23 @@ function flushGestureAnimations(): void {
|
||||
if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lanes = pendingEffectsLanes;
|
||||
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
// Update the new commitEndTime to when we started the animation.
|
||||
recordCommitEndTime();
|
||||
logStartViewTransitionYieldPhase(
|
||||
pendingEffectsRenderEndTime,
|
||||
commitEndTime,
|
||||
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
|
||||
animatingTask,
|
||||
);
|
||||
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
|
||||
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
|
||||
}
|
||||
}
|
||||
|
||||
pendingEffectsStatus = NO_PENDING_EFFECTS;
|
||||
const root = pendingEffectsRoot;
|
||||
const finishedWork = pendingFinishedWork;
|
||||
@@ -4282,6 +4372,10 @@ function flushGestureAnimations(): void {
|
||||
ReactSharedInternals.T = prevTransition;
|
||||
}
|
||||
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
finalizeRender(lanes, commitEndTime);
|
||||
}
|
||||
|
||||
// Now that we've rendered this lane. Start working on the next lane.
|
||||
ensureRootIsScheduled(root);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@ import type {CapturedValue} from './ReactCapturedValue';
|
||||
import {
|
||||
isTransitionLane,
|
||||
isBlockingLane,
|
||||
isSyncLane,
|
||||
isGestureRender,
|
||||
includesTransitionLane,
|
||||
includesBlockingLane,
|
||||
includesSyncLane,
|
||||
NoLanes,
|
||||
} from './ReactFiberLane';
|
||||
|
||||
@@ -76,6 +75,18 @@ export let blockingEventTime: number = -1.1; // Event timeStamp of the first set
|
||||
export let blockingEventType: null | string = null; // Event type of the first setState.
|
||||
export let blockingEventIsRepeat: boolean = false;
|
||||
export let blockingSuspendedTime: number = -1.1;
|
||||
|
||||
export let gestureClampTime: number = -0;
|
||||
export let gestureUpdateTime: number = -1.1; // First setOptimistic scheduled inside startGestureTransition.
|
||||
export let gestureUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
|
||||
export let gestureUpdateType: UpdateType = 0;
|
||||
export let gestureUpdateMethodName: null | string = null; // The name of the method that caused first gesture update.
|
||||
export let gestureUpdateComponentName: null | string = null; // The name of the component where first gesture update happened.
|
||||
export let gestureEventTime: number = -1.1; // Event timeStamp of the first setState.
|
||||
export let gestureEventType: null | string = null; // Event type of the first setState.
|
||||
export let gestureEventIsRepeat: boolean = false;
|
||||
export let gestureSuspendedTime: number = -1.1;
|
||||
|
||||
// TODO: This should really be one per Transition lane.
|
||||
export let transitionClampTime: number = -0;
|
||||
export let transitionStartTime: number = -1.1; // First startTransition call before setState.
|
||||
@@ -114,7 +125,26 @@ export function startUpdateTimerByLane(
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
if (isSyncLane(lane) || isBlockingLane(lane)) {
|
||||
if (isGestureRender(lane)) {
|
||||
if (gestureUpdateTime < 0) {
|
||||
gestureUpdateTime = now();
|
||||
gestureUpdateTask = createTask(method);
|
||||
gestureUpdateMethodName = method;
|
||||
if (__DEV__ && fiber != null) {
|
||||
gestureUpdateComponentName = getComponentNameFromFiber(fiber);
|
||||
}
|
||||
const newEventTime = resolveEventTimeStamp();
|
||||
const newEventType = resolveEventType();
|
||||
if (
|
||||
newEventTime !== gestureEventTime ||
|
||||
newEventType !== gestureEventType
|
||||
) {
|
||||
gestureEventIsRepeat = false;
|
||||
}
|
||||
gestureEventTime = newEventTime;
|
||||
gestureEventType = newEventType;
|
||||
}
|
||||
} else if (isBlockingLane(lane)) {
|
||||
if (blockingUpdateTime < 0) {
|
||||
blockingUpdateTime = now();
|
||||
blockingUpdateTask = createTask(method);
|
||||
@@ -220,7 +250,13 @@ export function startPingTimerByLanes(lanes: Lanes): void {
|
||||
// Mark the update time and clamp anything before it because we don't want
|
||||
// to show the event time for pings but we also don't want to clear it
|
||||
// because we still need to track if this was a repeat.
|
||||
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
|
||||
if (isGestureRender(lanes)) {
|
||||
if (gestureUpdateTime < 0) {
|
||||
gestureClampTime = gestureUpdateTime = now();
|
||||
gestureUpdateTask = createTask('Promise Resolved');
|
||||
gestureUpdateType = PINGED_UPDATE;
|
||||
}
|
||||
} else if (includesBlockingLane(lanes)) {
|
||||
if (blockingUpdateTime < 0) {
|
||||
blockingClampTime = blockingUpdateTime = now();
|
||||
blockingUpdateTask = createTask('Promise Resolved');
|
||||
@@ -239,7 +275,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
|
||||
if (isGestureRender(lanes)) {
|
||||
gestureSuspendedTime = renderEndTime;
|
||||
} else if (includesBlockingLane(lanes)) {
|
||||
blockingSuspendedTime = renderEndTime;
|
||||
} else if (includesTransitionLane(lanes)) {
|
||||
transitionSuspendedTime = renderEndTime;
|
||||
@@ -293,6 +331,28 @@ export function clearTransitionTimers(): void {
|
||||
transitionClampTime = now();
|
||||
}
|
||||
|
||||
export function hasScheduledGestureTransitionWork(): boolean {
|
||||
// If we have call setOptimistic on a gesture
|
||||
return gestureUpdateTime > -1;
|
||||
}
|
||||
|
||||
export function clearGestureTimers(): void {
|
||||
gestureUpdateTime = -1.1;
|
||||
gestureUpdateType = 0;
|
||||
gestureSuspendedTime = -1.1;
|
||||
gestureEventIsRepeat = true;
|
||||
gestureClampTime = now();
|
||||
}
|
||||
|
||||
export function clearGestureUpdates(): void {
|
||||
// Same as clearGestureTimers but doesn't reset the clamp time because we didn't
|
||||
// actually emit a render.
|
||||
gestureUpdateTime = -1.1;
|
||||
gestureUpdateType = 0;
|
||||
gestureSuspendedTime = -1.1;
|
||||
gestureEventIsRepeat = true;
|
||||
}
|
||||
|
||||
export function clampBlockingTimers(finalTime: number): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
@@ -303,6 +363,16 @@ export function clampBlockingTimers(finalTime: number): void {
|
||||
blockingClampTime = finalTime;
|
||||
}
|
||||
|
||||
export function clampGestureTimers(finalTime: number): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
// If we had new updates come in while we were still rendering or committing, we don't want
|
||||
// those update times to create overlapping tracks in the performance timeline so we clamp
|
||||
// them to the end of the commit phase.
|
||||
gestureClampTime = finalTime;
|
||||
}
|
||||
|
||||
export function clampTransitionTimers(finalTime: number): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
NoEventPriority,
|
||||
type EventPriority,
|
||||
} from 'react-reconciler/src/ReactEventPriorities';
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version.
|
||||
export const rendererPackageName = 'react-test-renderer';
|
||||
@@ -446,9 +447,13 @@ export function startGestureTransition(
|
||||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
91
yarn.lock
91
yarn.lock
@@ -3227,6 +3227,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/sourcemap-codec@1.5.5":
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
@@ -8269,7 +8274,7 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0:
|
||||
dependencies:
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
"eslint-v7@npm:eslint@^7.7.0", eslint@^7.7.0:
|
||||
"eslint-v7@npm:eslint@^7.7.0":
|
||||
version "7.32.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
|
||||
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
|
||||
@@ -8468,6 +8473,52 @@ eslint@8.57.0:
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
eslint@^7.7.0:
|
||||
version "7.32.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
|
||||
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "7.12.11"
|
||||
"@eslint/eslintrc" "^0.4.3"
|
||||
"@humanwhocodes/config-array" "^0.5.0"
|
||||
ajv "^6.10.0"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
debug "^4.0.1"
|
||||
doctrine "^3.0.0"
|
||||
enquirer "^2.3.5"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^2.1.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
espree "^7.3.1"
|
||||
esquery "^1.4.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob-parent "^5.1.2"
|
||||
globals "^13.6.0"
|
||||
ignore "^4.0.6"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
js-yaml "^3.13.1"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.0.4"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.1"
|
||||
progress "^2.0.0"
|
||||
regexpp "^3.1.0"
|
||||
semver "^7.2.1"
|
||||
strip-ansi "^6.0.0"
|
||||
strip-json-comments "^3.1.0"
|
||||
table "^6.0.9"
|
||||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
espree@10.0.1, espree@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f"
|
||||
@@ -14269,7 +14320,7 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
"prettier-2@npm:prettier@^2", prettier@^2.5.1:
|
||||
"prettier-2@npm:prettier@^2":
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
@@ -14284,6 +14335,11 @@ prettier@^1.19.1:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
prettier@^2.5.1:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
pretty-format@^29.4.1:
|
||||
version "29.4.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.1.tgz#0da99b532559097b8254298da7c75a0785b1751c"
|
||||
@@ -16158,7 +16214,7 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -16193,6 +16249,15 @@ string-width@^4.0.0:
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
@@ -16253,7 +16318,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -16281,6 +16346,13 @@ strip-ansi@^5.1.0:
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
@@ -17889,7 +17961,7 @@ workerize-loader@^2.0.2:
|
||||
dependencies:
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -17907,6 +17979,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
||||
Reference in New Issue
Block a user