Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
1895becd54 [compiler] Fix fbt for the ∞th time
We now do a single pass over the HIR, building up two data structures:
* One tracks values that are known macro tags or macro calls.
* One tracks operands of macro-related instructions so that we can later group them.

After building up these data structures, we do a pass over the latter structure. For each macro call instruction, we recursively traverse its operands to ensure they're in the same scope. Thus, something like `fbt('hello' + fbt.param(foo(), "..."))` will correctly merge the fbt call, the `+` binary expression, the `fbt.param()` call, and `foo()` into a single scope.
2025-10-15 16:12:00 -07:00
66 changed files with 416 additions and 949 deletions

View File

@@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features

View File

@@ -52,8 +52,8 @@
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -988,7 +988,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'void-use-memo',
description:
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
preset: LintRulePreset.RecommendedLatest,
};
}

View File

@@ -6,7 +6,7 @@
*/
import * as t from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {
CompilerDiagnostic,
CompilerError,
@@ -20,7 +20,7 @@ import {
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([

View File

@@ -6,8 +6,8 @@
*/
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
@@ -659,7 +659,7 @@ export const EnvironmentConfigSchema = z.object({
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(true),
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope

View File

@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod/v4';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';

View File

@@ -988,7 +988,7 @@ export function createTemporaryPlace(
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
reactive: false,
effect: Effect.Unknown,
loc,
loc: GeneratedSource,
};
}

View File

@@ -6,7 +6,7 @@
*/
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {
EffectSchema,

View File

@@ -11,6 +11,7 @@ import {
CallExpression,
Effect,
Environment,
FinishMemoize,
FunctionExpression,
HIRFunction,
IdentifierId,
@@ -24,6 +25,7 @@ import {
Place,
PropertyLoad,
SpreadPattern,
StartMemoize,
TInstruction,
getHookKindForType,
makeInstructionId,
@@ -182,52 +184,36 @@ function makeManualMemoizationMarkers(
depsList: Array<ManualMemoDependency> | null,
memoDecl: Place,
manualMemoId: number,
): [Array<Instruction>, Array<Instruction>] {
const temp = createTemporaryPlace(env, memoDecl.loc);
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
return [
[
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, fnExpr.loc),
value: {
kind: 'StartMemoize',
manualMemoId,
/*
* Use deps list from source instead of inferred deps
* as dependencies
*/
deps: depsList,
loc: fnExpr.loc,
},
effects: null,
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, fnExpr.loc),
value: {
kind: 'StartMemoize',
manualMemoId,
/*
* Use deps list from source instead of inferred deps
* as dependencies
*/
deps: depsList,
loc: fnExpr.loc,
},
],
[
{
id: makeInstructionId(0),
lvalue: {...temp},
value: {
kind: 'LoadLocal',
place: {...memoDecl},
loc: memoDecl.loc,
},
effects: null,
loc: memoDecl.loc,
effects: null,
loc: fnExpr.loc,
},
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, fnExpr.loc),
value: {
kind: 'FinishMemoize',
manualMemoId,
decl: {...memoDecl},
loc: fnExpr.loc,
},
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, memoDecl.loc),
value: {
kind: 'FinishMemoize',
manualMemoId,
decl: {...temp},
loc: memoDecl.loc,
},
effects: null,
loc: memoDecl.loc,
},
],
effects: null,
loc: fnExpr.loc,
},
];
}
@@ -423,7 +409,10 @@ export function dropManualMemoization(
* LoadLocal fnArg
* - (if validation is enabled) collect manual memoization markers
*/
const queuedInserts: Map<InstructionId, Array<Instruction>> = new Map();
const queuedInserts: Map<
InstructionId,
TInstruction<StartMemoize> | TInstruction<FinishMemoize>
> = new Map();
for (const [_, block] of func.body.blocks) {
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i]!;
@@ -449,6 +438,40 @@ export function dropManualMemoization(
continue;
}
/**
* Bailout on void return useMemos. This is an anti-pattern where code might be using
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
* values.
*/
if (
func.env.config.validateNoVoidUseMemo &&
manualMemo.kind === 'useMemo'
) {
const funcToCheck = sidemap.functions.get(
fnPlace.identifier.id,
)?.value;
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
@@ -534,11 +557,11 @@ export function dropManualMemoization(
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i];
const insertInstructions = queuedInserts.get(instr.id);
if (insertInstructions != null) {
const insertInstr = queuedInserts.get(instr.id);
if (insertInstr != null) {
nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
nextInstructions.push(instr);
nextInstructions.push(...insertInstructions);
nextInstructions.push(insertInstr);
} else if (nextInstructions != null) {
nextInstructions.push(instr);
}
@@ -606,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
return optionals;
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -77,15 +77,6 @@ class Transform extends ReactiveFunctionTransform<boolean> {
}
break;
}
case 'FinishMemoize': {
if (
!withinScope &&
this.alwaysInvalidatingValues.has(value.decl.identifier)
) {
value.pruned = true;
}
break;
}
}
return {kind: 'keep'};
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
CompilationMode,

View File

@@ -10,37 +10,16 @@ import {
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {
FunctionExpression,
HIRFunction,
IdentifierId,
SourceLocation,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
for (const [, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (unusedUseMemos.size !== 0) {
/**
* Most of the time useMemo results are referenced immediately. Don't bother
* scanning instruction operands for useMemos unless there is an as-yet-unused
* useMemo.
*/
for (const operand of eachInstructionValueOperand(value)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
switch (value.kind) {
case 'LoadGlobal': {
if (value.binding.name === 'useMemo') {
@@ -66,8 +45,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
case 'CallExpression': {
// Is the function being called useMemo, with at least 1 argument?
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const isUseMemo = useMemos.has(callee.identifier.id);
value.kind === 'CallExpression'
? value.callee.identifier.id
: value.property.identifier.id;
const isUseMemo = useMemos.has(callee);
if (!isUseMemo || value.args.length === 0) {
continue;
}
@@ -123,103 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: body.loc,
message: 'useMemo() callbacks must return a value',
}),
);
} else {
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
}
}
break;
}
}
}
if (unusedUseMemos.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
}
if (unusedUseMemos.size !== 0) {
/**
* Basic check for unused memos, where the result of the call is never referenced. This runs
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
*
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
*/
for (const loc of unusedUseMemos.values()) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() result is unused',
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc,
message: 'useMemo() result is unused',
}),
);
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
switch (value.kind) {
case 'StoreContext': {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not reassign variables declared outside of the callback',
description:
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
suggestions: null,
}).withDetails({
kind: 'error',
loc: value.lvalue.place.loc,
message: 'Cannot reassign variable',
}),
);
break;
}
}
}
}
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -29,7 +29,7 @@ Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$47[7:9]{reactive}' is const.
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |

View File

@@ -1,38 +0,0 @@
## Input
```javascript
function Component() {
let x;
const y = useMemo(() => {
let z;
x = [];
z = true;
return z;
}, []);
return [x, y];
}
```
## Error
```
Found 1 error:
Error: useMemo() callbacks may not reassign variables declared outside of the callback
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
error.invalid-reassign-variable-in-usememo.ts:5:4
3 | const y = useMemo(() => {
4 | let z;
> 5 | x = [];
| ^ Cannot reassign variable
6 | z = true;
7 | return z;
8 | }, []);
```

View File

@@ -1,10 +0,0 @@
function Component() {
let x;
const y = useMemo(() => {
let z;
x = [];
z = true;
return z;
}, []);
return [x, y];
}

View File

@@ -0,0 +1,64 @@
## Input
```javascript
// @validateNoVoidUseMemo
function Component() {
const value = useMemo(() => {
console.log('computing');
}, []);
const value2 = React.useMemo(() => {
console.log('computing');
}, []);
return (
<div>
{value}
{value2}
</div>
);
}
```
## Error
```
Found 2 errors:
Error: useMemo() callbacks must return a value
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
error.useMemo-no-return-value.ts:3:16
1 | // @validateNoVoidUseMemo
2 | function Component() {
> 3 | const value = useMemo(() => {
| ^^^^^^^^^^^^^^^
> 4 | console.log('computing');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 5 | }, []);
| ^^^^^^^^^ useMemo() callbacks must return a value
6 | const value2 = React.useMemo(() => {
7 | console.log('computing');
8 | }, []);
Error: useMemo() callbacks must return a value
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
error.useMemo-no-return-value.ts:6:17
4 | console.log('computing');
5 | }, []);
> 6 | const value2 = React.useMemo(() => {
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | console.log('computing');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, []);
| ^^^^^^^^^ useMemo() callbacks must return a value
9 | return (
10 | <div>
11 | {value}
```

View File

@@ -1,4 +1,4 @@
// @validateNoVoidUseMemo @loggerTestOnly
// @validateNoVoidUseMemo
function Component() {
const value = useMemo(() => {
console.log('computing');

View File

@@ -1,41 +0,0 @@
## Input
```javascript
// @validateNoVoidUseMemo @loggerTestOnly
function Component() {
useMemo(() => {
return [];
}, []);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,7 +0,0 @@
// @validateNoVoidUseMemo @loggerTestOnly
function Component() {
useMemo(() => {
return [];
}, []);
return <div />;
}

View File

@@ -1,59 +0,0 @@
## Input
```javascript
// @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const value = useMemo(() => {
console.log('computing');
}, []);
const value2 = React.useMemo(() => {
console.log('computing');
}, []);
return (
<div>
{value}
{value2}
</div>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const $ = _c(1);
console.log("computing");
console.log("computing");
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<div>
{undefined}
{undefined}
</div>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,33 +0,0 @@
## Input
```javascript
// @loggerTestOnly
function component(a) {
let x = useMemo(() => {
mutate(a);
}, []);
return x;
}
```
## Code
```javascript
// @loggerTestOnly
function component(a) {
mutate(a);
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useHook} from 'shared-runtime';
// useMemo values may not be memoized in Forget output if we
// infer that their deps always invalidate.
// This is technically a false positive as the useMemo in source
// was effectively a no-op
function useFoo(props) {
const x = [];
useHook();
x.push(props);
return useMemo(() => [x], [x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
};
```
## Error
```
Found 1 error:
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
error.false-positive-useMemo-dropped-infer-always-invalidating.ts:15:9
13 | x.push(props);
14 |
> 15 | return useMemo(() => [x], [x]);
| ^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
16 | }
17 |
18 | export const FIXTURE_ENTRYPOINT = {
```

View File

@@ -0,0 +1,21 @@
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useHook} from 'shared-runtime';
// useMemo values may not be memoized in Forget output if we
// infer that their deps always invalidate.
// This is technically a false positive as the useMemo in source
// was effectively a no-op
function useFoo(props) {
const x = [];
useHook();
x.push(props);
return useMemo(() => [x], [x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
};

View File

@@ -1,56 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useHook} from 'shared-runtime';
// If we can prove that a useMemo was ineffective because it would always invalidate,
// then we shouldn't throw a "couldn't preserve existing memoization" error
// TODO: consider reporting a separate error to the user for this case, if you're going
// to memoize manually, then you probably want to know that it's a no-op
function useFoo(props) {
const x = [];
useHook();
x.push(props);
return useMemo(() => [x], [x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
};
```
## Code
```javascript
// @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { useHook } from "shared-runtime";
// If we can prove that a useMemo was ineffective because it would always invalidate,
// then we shouldn't throw a "couldn't preserve existing memoization" error
// TODO: consider reporting a separate error to the user for this case, if you're going
// to memoize manually, then you probably want to know that it's a no-op
function useFoo(props) {
const x = [];
useHook();
x.push(props);
return [x];
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
};
```
### Eval output
(kind: ok) [[{}]]

View File

@@ -1,21 +0,0 @@
// @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {useHook} from 'shared-runtime';
// If we can prove that a useMemo was ineffective because it would always invalidate,
// then we shouldn't throw a "couldn't preserve existing memoization" error
// TODO: consider reporting a separate error to the user for this case, if you're going
// to memoize manually, then you probably want to know that it's a no-op
function useFoo(props) {
const x = [];
useHook();
x.push(props);
return useMemo(() => [x], [x]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{}],
};

View File

@@ -1,98 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
import {identity, useIdentity} from 'shared-runtime';
// Adapted from https://github.com/facebook/react/issues/34750
function useLocalCampaignBySlug(slug: string) {
const campaigns = useIdentity({a: {slug: 'a', name: 'campaign'}});
// The useMemo result is never assigned to a local so we did not previously ensure
// that there was a variable declaration for it when promoting the result temporary
return useMemo(() => {
for (const id of Object.keys(campaigns)) {
const campaign = campaigns[id];
if (campaign.slug === slug) {
return identity(campaign);
}
}
return null;
}, [campaigns, slug]);
}
function Component() {
const campaign = useLocalCampaignBySlug('a');
return <div>{campaign.name}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import { identity, useIdentity } from "shared-runtime";
// Adapted from https://github.com/facebook/react/issues/34750
function useLocalCampaignBySlug(slug) {
const $ = _c(4);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { a: { slug: "a", name: "campaign" } };
$[0] = t0;
} else {
t0 = $[0];
}
const campaigns = useIdentity(t0);
let t1;
if ($[1] !== campaigns || $[2] !== slug) {
bb0: {
for (const id of Object.keys(campaigns)) {
const campaign = campaigns[id];
if (campaign.slug === slug) {
t1 = identity(campaign);
break bb0;
}
}
t1 = null;
}
$[1] = campaigns;
$[2] = slug;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function Component() {
const $ = _c(2);
const campaign = useLocalCampaignBySlug("a");
let t0;
if ($[0] !== campaign.name) {
t0 = <div>{campaign.name}</div>;
$[0] = campaign.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div>campaign</div>

View File

@@ -1,28 +0,0 @@
import {useMemo} from 'react';
import {identity, useIdentity} from 'shared-runtime';
// Adapted from https://github.com/facebook/react/issues/34750
function useLocalCampaignBySlug(slug: string) {
const campaigns = useIdentity({a: {slug: 'a', name: 'campaign'}});
// The useMemo result is never assigned to a local so we did not previously ensure
// that there was a variable declaration for it when promoting the result temporary
return useMemo(() => {
for (const id of Object.keys(campaigns)) {
const campaign = campaigns[id];
if (campaign.slug === slug) {
return identity(campaign);
}
}
return null;
}, [campaigns, slug]);
}
function Component() {
const campaign = useLocalCampaignBySlug('a');
return <div>{campaign.name}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @validateNoVoidUseMemo:false
function Component(props) {
const item = props.item;
const thumbnails = [];
@@ -23,7 +22,7 @@ function Component(props) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const item = props.item;

View File

@@ -1,4 +1,3 @@
// @validateNoVoidUseMemo:false
function Component(props) {
const item = props.item;
const thumbnails = [];

View File

@@ -6,7 +6,6 @@ function Component(props) {
const x = useMemo(() => {
if (props.cond) {
if (props.cond) {
return props.value;
}
}
}, [props.cond]);
@@ -25,18 +24,10 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
function Component(props) {
let t0;
bb0: {
if (props.cond) {
if (props.cond) {
if (props.cond) {
t0 = props.value;
break bb0;
}
}
t0 = undefined;
}
const x = t0;
return x;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -2,7 +2,6 @@ function Component(props) {
const x = useMemo(() => {
if (props.cond) {
if (props.cond) {
return props.value;
}
}
}, [props.cond]);

View File

@@ -0,0 +1,22 @@
## Input
```javascript
function component(a) {
let x = useMemo(() => {
mutate(a);
}, []);
return x;
}
```
## Code
```javascript
function component(a) {
mutate(a);
}
```

View File

@@ -120,15 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
return <Child x={state} />;
}`,
errors: [
makeTestCaseError('useMemo() callbacks must return a value'),
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
],
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
},
{
name: 'Pipeline errors are reported',

View File

@@ -15,8 +15,8 @@
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"hermes-parser": "^0.25.1",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.22.4",

View File

@@ -10,14 +10,7 @@ import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [
'@babel/core',
'hermes-parser',
'zod',
'zod/v4',
'zod-validation-error',
'zod-validation-error/v4',
],
external: ['@babel/core', 'hermes-parser', 'zod', 'zod-validation-error'],
splitting: false,
sourcemap: false,
dts: false,

View File

@@ -17,8 +17,8 @@
"fast-glob": "^3.3.2",
"ora": "5.4.1",
"yargs": "^17.7.2",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {},
"engines": {

View File

@@ -18,9 +18,7 @@ export default defineConfig({
'ora',
'yargs',
'zod',
'zod/v4',
'zod-validation-error',
'zod-validation-error/v4',
],
splitting: false,
sourcemap: false,

View File

@@ -24,7 +24,7 @@
"html-to-text": "^9.0.5",
"prettier": "^3.3.3",
"puppeteer": "^24.7.2",
"zod": "^3.25.0 || ^4.0.0"
"zod": "^3.22.4 || ^4.0.0"
},
"devDependencies": {
"@types/html-to-text": "^9.0.4",

View File

@@ -7,7 +7,7 @@
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod/v4';
import {z} from 'zod';
import {compile, type PrintedCompilerPipelineValue} from './compiler';
import {
CompilerPipelineValue,

View File

@@ -37,9 +37,7 @@
"react": "0.0.0-experimental-4beb1fd8-20241118",
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"readline": "^1.3.0",
"yargs": "^17.7.1",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"yargs": "^17.7.1"
},
"devDependencies": {
"@babel/core": "^7.19.1",

View File

@@ -9,8 +9,8 @@ import {render} from '@testing-library/react';
import {JSDOM} from 'jsdom';
import React, {MutableRefObject} from 'react';
import util from 'util';
import {z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {initFbt, toJSON} from './shared-runtime';
/**

View File

@@ -11505,17 +11505,17 @@ zod-to-json-schema@^3.24.1:
resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz"
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
"zod-validation-error@^3.5.0 || ^4.0.0":
"zod-validation-error@^3.0.3 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
"zod@^3.22.4 || ^4.0.0":
version "4.1.11"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
zod@^3.23.8, zod@^3.24.1:
version "3.24.3"
resolved "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz"
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
"zod@^3.25.0 || ^4.0.0":
version "4.1.12"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==

View File

@@ -1555,17 +1555,6 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
{
code: normalizeIndent`
// Invalid because useEffectEvent is being passed down
function MyComponent({ theme }) {
return <Child onClick={useEffectEvent(() => {
showNotification(theme);
})} />;
}
`,
errors: [{...useEffectEventError(null, false), line: 4}],
},
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
@@ -1737,14 +1726,6 @@ function classError(hook) {
}
function useEffectEventError(fn, called) {
if (fn === null) {
return {
message:
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
` It cannot be passed down.`,
};
}
return {
message:
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +

View File

@@ -42,8 +42,8 @@
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"hermes-parser": "^0.25.1",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.11.4",

View File

@@ -171,15 +171,7 @@ function isUseEffectEventIdentifier(node: Node): boolean {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
function useEffectEventError(fn: string | null, called: boolean): string {
// no function identifier, i.e. it is not assigned to a variable
if (fn === null) {
return (
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
` It cannot be passed down.`
);
}
function useEffectEventError(fn: string, called: boolean): string {
return (
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'Effects and Effect Events in the same component.' +
@@ -780,22 +772,6 @@ const rule = {
// comparison later when we exit
lastEffect = node;
}
// Specifically disallow <Child onClick={useEffectEvent(...)} /> because this
// case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable
if (
isUseEffectEventIdentifier(nodeWithoutNamespace) &&
node.parent?.type !== 'VariableDeclarator' &&
// like in other hooks, calling useEffectEvent at component's top level without assignment is valid
node.parent?.type !== 'ExpressionStatement'
) {
const message = useEffectEventError(null, false);
context.report({
node,
message,
});
}
},
Identifier(node) {

View File

@@ -1546,7 +1546,7 @@ describe('Store', () => {
▸ <Wrapper>
`);
const deepestedNodeID = agent.getIDForHostInstance(ref.current).id;
const deepestedNodeID = agent.getIDForHostInstance(ref.current);
await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
expect(store).toMatchInlineSnapshot(`

View File

@@ -455,10 +455,7 @@ export default class Agent extends EventEmitter<{
return renderer.getInstanceAndStyle(id);
}
getIDForHostInstance(
target: HostInstance,
onlySuspenseNodes?: boolean,
): null | {id: number, rendererID: number} {
getIDForHostInstance(target: HostInstance): number | null {
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
@@ -466,14 +463,9 @@ export default class Agent extends EventEmitter<{
(rendererID: any)
]: any): RendererInterface);
try {
const id = onlySuspenseNodes
? renderer.getSuspenseNodeIDForHostInstance(target)
: renderer.getElementIDForHostInstance(target);
if (id !== null) {
return {
id: id,
rendererID: +rendererID,
};
const match = renderer.getElementIDForHostInstance(target);
if (match != null) {
return match;
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
@@ -486,7 +478,6 @@ export default class Agent extends EventEmitter<{
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
let bestRendererID: number = 0;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
@@ -500,7 +491,6 @@ export default class Agent extends EventEmitter<{
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
bestRendererID = +rendererID;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
@@ -508,21 +498,12 @@ export default class Agent extends EventEmitter<{
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
bestRendererID = +rendererID;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = onlySuspenseNodes
? bestRenderer.getSuspenseNodeIDForHostInstance(bestMatch)
: bestRenderer.getElementIDForHostInstance(bestMatch);
if (id !== null) {
return {
id,
rendererID: bestRendererID,
};
}
return bestRenderer.getElementIDForHostInstance(bestMatch);
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
@@ -533,14 +514,65 @@ export default class Agent extends EventEmitter<{
}
getComponentNameForHostInstance(target: HostInstance): string | null {
const match = this.getIDForHostInstance(target);
if (match !== null) {
const renderer = ((this._rendererInterfaces[
(match.rendererID: any)
]: any): RendererInterface);
return renderer.getDisplayNameForElementID(match.id);
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const id = renderer.getElementIDForHostInstance(target);
if (id) {
return renderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
}
return null;
}
getBackendVersion: () => void = () => {
@@ -939,9 +971,9 @@ export default class Agent extends EventEmitter<{
};
selectNode(target: HostInstance): void {
const match = this.getIDForHostInstance(target);
if (match !== null) {
this._bridge.send('selectElement', match.id);
const id = this.getIDForHostInstance(target);
if (id !== null) {
this._bridge.send('selectElement', id);
}
}

View File

@@ -2693,10 +2693,10 @@ export function attach(
pushOperation(rects.length);
for (let i = 0; i < rects.length; ++i) {
const rect = rects[i];
pushOperation(Math.round(rect.x * 1000));
pushOperation(Math.round(rect.y * 1000));
pushOperation(Math.round(rect.width * 1000));
pushOperation(Math.round(rect.height * 1000));
pushOperation(Math.round(rect.x));
pushOperation(Math.round(rect.y));
pushOperation(Math.round(rect.width));
pushOperation(Math.round(rect.height));
}
}
}
@@ -2765,10 +2765,10 @@ export function attach(
pushOperation(rects.length);
for (let i = 0; i < rects.length; ++i) {
const rect = rects[i];
pushOperation(Math.round(rect.x * 1000));
pushOperation(Math.round(rect.y * 1000));
pushOperation(Math.round(rect.width * 1000));
pushOperation(Math.round(rect.height * 1000));
pushOperation(Math.round(rect.x));
pushOperation(Math.round(rect.y));
pushOperation(Math.round(rect.width));
pushOperation(Math.round(rect.height));
}
}
}
@@ -5793,28 +5793,7 @@ export function attach(
return null;
}
if (devtoolsInstance.kind === FIBER_INSTANCE) {
const fiber = devtoolsInstance.data;
if (fiber.tag === HostRoot) {
// The only reason you'd inspect a HostRoot is to show it as a SuspenseNode.
return 'Initial Paint';
}
if (fiber.tag === SuspenseComponent || fiber.tag === ActivityComponent) {
// For Suspense and Activity components, we can show a better name
// by using the name prop or their owner.
const props = fiber.memoizedProps;
if (props.name != null) {
return props.name;
}
const owner = getUnfilteredOwner(fiber);
if (owner != null) {
if (typeof owner.tag === 'number') {
return getDisplayNameForFiber((owner: any));
} else {
return owner.name || '';
}
}
}
return getDisplayNameForFiber(fiber);
return getDisplayNameForFiber(devtoolsInstance.data);
} else {
return devtoolsInstance.data.name || '';
}
@@ -5855,28 +5834,6 @@ export function attach(
return null;
}
function getSuspenseNodeIDForHostInstance(
publicInstance: HostInstance,
): number | null {
const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance);
if (instance !== undefined) {
// Pick nearest unfiltered SuspenseNode instance.
let suspenseInstance = instance;
while (
suspenseInstance.suspenseNode === null ||
suspenseInstance.kind === FILTERED_FIBER_INSTANCE
) {
if (suspenseInstance.parent === null) {
// We shouldn't get here since we'll always have a suspenseNode at the root.
return null;
}
suspenseInstance = suspenseInstance.parent;
}
return suspenseInstance.id;
}
return null;
}
function getElementAttributeByPath(
id: number,
path: Array<string | number>,
@@ -8673,7 +8630,6 @@ export function attach(
getDisplayNameForElementID,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getSuspenseNodeIDForHostInstance,
getInstanceAndStyle,
getOwnersList,
getPathForElement,

View File

@@ -169,9 +169,6 @@ export function attach(
getElementIDForHostInstance() {
return null;
},
getSuspenseNodeIDForHostInstance() {
return null;
},
getInstanceAndStyle() {
return {
instance: null,

View File

@@ -1269,9 +1269,6 @@ export function attach(
getDisplayNameForElementID,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getSuspenseNodeIDForHostInstance(id: number): null {
return null;
},
getInstanceAndStyle,
findHostInstancesForElementID: (id: number) => {
const hostInstance = findHostInstanceForInternalID(id);

View File

@@ -427,7 +427,6 @@ export type RendererInterface = {
getComponentStack?: GetComponentStack,
getNearestMountedDOMNode: (component: Element) => Element | null,
getElementIDForHostInstance: GetElementIDForHostInstance,
getSuspenseNodeIDForHostInstance: GetElementIDForHostInstance,
getDisplayNameForElementID: GetDisplayNameForElementID,
getInstanceAndStyle(id: number): InstanceAndStyle,
getProfilingData(): ProfilingDataBackend,

View File

@@ -20,7 +20,6 @@ import type {RendererInterface} from '../../types';
// That is done by the React Native Inspector component.
let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
let inspectOnlySuspenseNodes = false;
export default function setupHighlighter(
bridge: BackendBridge,
@@ -34,8 +33,7 @@ export default function setupHighlighter(
bridge.addListener('startInspectingHost', startInspectingHost);
bridge.addListener('stopInspectingHost', stopInspectingHost);
function startInspectingHost(onlySuspenseNodes: boolean) {
inspectOnlySuspenseNodes = onlySuspenseNodes;
function startInspectingHost() {
registerListenersOnWindow(window);
}
@@ -365,37 +363,9 @@ export default function setupHighlighter(
}
}
if (inspectOnlySuspenseNodes) {
// For Suspense nodes we want to highlight not the actual target but the nodes
// that are the root of the Suspense node.
// TODO: Consider if we should just do the same for other elements because the
// hovered node might just be one child of many in the Component.
const match = agent.getIDForHostInstance(
target,
inspectOnlySuspenseNodes,
);
if (match !== null) {
const renderer = agent.rendererInterfaces[match.rendererID];
if (renderer == null) {
console.warn(
`Invalid renderer id "${match.rendererID}" for element "${match.id}"`,
);
return;
}
highlightHostInstance({
displayName: renderer.getDisplayNameForElementID(match.id),
hideAfterTimeout: false,
id: match.id,
openBuiltinElementsPanel: false,
rendererID: match.rendererID,
scrollIntoView: false,
});
}
} else {
// Don't pass the name explicitly.
// It will be inferred from DOM tag and Fiber owner.
showOverlay([target], null, agent, false);
}
// Don't pass the name explicitly.
// It will be inferred from DOM tag and Fiber owner.
showOverlay([target], null, agent, false);
}
function onPointerUp(event: MouseEvent) {
@@ -404,9 +374,9 @@ export default function setupHighlighter(
}
const selectElementForNode = (node: HTMLElement) => {
const match = agent.getIDForHostInstance(node, inspectOnlySuspenseNodes);
if (match !== null) {
bridge.send('selectElement', match.id);
const id = agent.getIDForHostInstance(node);
if (id !== null) {
bridge.send('selectElement', id);
}
};

View File

@@ -266,7 +266,7 @@ type FrontendEvents = {
savedPreferences: [SavedPreferencesParams],
setTraceUpdatesEnabled: [boolean],
shutdown: [],
startInspectingHost: [boolean],
startInspectingHost: [],
startProfiling: [StartProfilingParams],
stopInspectingHost: [],
scrollToHostInstance: [ScrollToHostInstance],

View File

@@ -1587,10 +1587,10 @@ export default class Store extends EventEmitter<{
} else {
rects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0] / 1000;
const y = operations[i + 1] / 1000;
const width = operations[i + 2] / 1000;
const height = operations[i + 3] / 1000;
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
rects.push({x, y, width, height});
i += 4;
}
@@ -1763,10 +1763,10 @@ export default class Store extends EventEmitter<{
} else {
nextRects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0] / 1000;
const y = operations[i + 1] / 1000;
const width = operations[i + 2] / 1000;
const height = operations[i + 3] / 1000;
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
nextRects.push({x, y, width, height});

View File

@@ -14,11 +14,7 @@ import Toggle from '../Toggle';
import ButtonIcon from '../ButtonIcon';
import {logEvent} from 'react-devtools-shared/src/Logger';
export default function InspectHostNodesToggle({
onlySuspenseNodes,
}: {
onlySuspenseNodes?: boolean,
}): React.Node {
export default function InspectHostNodesToggle(): React.Node {
const [isInspecting, setIsInspecting] = useState(false);
const bridge = useContext(BridgeContext);
@@ -28,7 +24,7 @@ export default function InspectHostNodesToggle({
if (isChecked) {
logEvent({event_name: 'inspect-element-button-clicked'});
bridge.send('startInspectingHost', !!onlySuspenseNodes);
bridge.send('startInspectingHost');
} else {
bridge.send('stopInspectingHost');
}

View File

@@ -36,14 +36,12 @@ function ScaledRect({
rect,
visible,
suspended,
adjust,
...props
}: {
className: string,
rect: Rect,
visible: boolean,
suspended: boolean,
adjust?: boolean,
...
}): React$Node {
const viewBox = useContext(ViewBox);
@@ -59,9 +57,8 @@ function ScaledRect({
data-visible={visible}
data-suspended={suspended}
style={{
// Shrink one pixel so that the bottom outline will line up with the top outline of the next one.
width: adjust ? 'calc(' + width + ' - 1px)' : width,
height: adjust ? 'calc(' + height + ' - 1px)' : height,
width,
height,
top: y,
left: x,
}}
@@ -163,7 +160,6 @@ function SuspenseRects({
className={styles.SuspenseRectsRect}
rect={rect}
data-highlighted={selected}
adjust={true}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onPointerOver={handlePointerOver}

View File

@@ -14,7 +14,6 @@ import {
useLayoutEffect,
useReducer,
useRef,
Fragment,
} from 'react';
import {
@@ -22,7 +21,6 @@ import {
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import ButtonIcon, {type IconType} from '../ButtonIcon';
import InspectHostNodesToggle from '../Components/InspectHostNodesToggle';
import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBoundary';
import InspectedElement from '../Components/InspectedElement';
import portaledContent from '../portaledContent';
@@ -158,7 +156,6 @@ function ToggleInspectedElement({
}
function SuspenseTab(_: {}) {
const store = useContext(StoreContext);
const {hideSettings} = useContext(OptionsContext);
const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
layoutReducer,
@@ -370,12 +367,6 @@ function SuspenseTab(_: {}) {
) : (
<ToggleTreeList dispatch={dispatch} state={state} />
)}
{store.supportsClickToInspect && (
<Fragment>
<InspectHostNodesToggle onlySuspenseNodes={true} />
<div className={styles.VRule} />
</Fragment>
)}
<div className={styles.SuspenseBreadcrumbs}>
<SuspenseBreadcrumbs />
</div>

View File

@@ -160,7 +160,9 @@ function SuspenseTimelineInput() {
onClick={skipForward}>
<ButtonIcon type={'skip-next'} />
</Button>
<div className={styles.SuspenseTimelineInput}>
<div
className={styles.SuspenseTimelineInput}
title={timelineIndex + '/' + max}>
<SuspenseScrubber
min={min}
max={max}

View File

@@ -205,27 +205,6 @@ export function pluralize(word: string): string {
return word;
}
// Bail out if it's already plural.
switch (word) {
case 'men':
case 'women':
case 'children':
case 'feet':
case 'teeth':
case 'mice':
case 'people':
return word;
}
if (
/(ches|shes|ses|xes|zes)$/i.test(word) ||
/[^s]ies$/i.test(word) ||
/ves$/i.test(word) ||
/[^s]s$/i.test(word)
) {
return word;
}
switch (word) {
case 'man':
return 'men';

View File

@@ -475,25 +475,23 @@ function loadSourceFiles(
const fetchPromise =
dedupedFetchPromises.get(runtimeSourceURL) ||
(runtimeSourceURL && !runtimeSourceURL.startsWith('<anonymous')
? fetchFileFunction(runtimeSourceURL).then(runtimeSourceCode => {
// TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps,
// because then we need to parse the full source file as an AST.
if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) {
throw Error('Source code too large to parse');
}
fetchFileFunction(runtimeSourceURL).then(runtimeSourceCode => {
// TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps,
// because then we need to parse the full source file as an AST.
if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) {
throw Error('Source code too large to parse');
}
if (__DEBUG__) {
console.groupCollapsed(
`loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`,
);
console.log(runtimeSourceCode);
console.groupEnd();
}
if (__DEBUG__) {
console.groupCollapsed(
`loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`,
);
console.log(runtimeSourceCode);
console.groupEnd();
}
return runtimeSourceCode;
})
: Promise.reject(new Error('Empty url')));
return runtimeSourceCode;
});
dedupedFetchPromises.set(runtimeSourceURL, fetchPromise);
setterPromises.push(

View File

@@ -52,9 +52,6 @@ export async function symbolicateSource(
lineNumber: number, // 1-based
columnNumber: number, // 1-based
): Promise<SourceMappedLocation | null> {
if (!sourceURL || sourceURL.startsWith('<anonymous')) {
return null;
}
const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
if (resource == null) {
return null;

View File

@@ -57,14 +57,6 @@ function getOwner() {
return null;
}
// v8 (Chromium, Node.js) defaults to 10
// SpiderMonkey (Firefox) does not support Error.stackTraceLimit
// JSC (Safari) defaults to 100
// The lower the limit, the more likely we'll not reach react_stack_bottom_frame
// The higher the limit, the slower Error() is when not inspecting with a debugger.
// When inspecting with a debugger, Error.stackTraceLimit has no impact on Error() performance (in v8).
const ownerStackTraceLimit = 10;
/** @noinline */
function UnknownOwner() {
/** @noinline */
@@ -360,24 +352,15 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren(
const trackActualOwner =
__DEV__ &&
ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit;
let debugStackDEV = false;
if (__DEV__) {
if (trackActualOwner) {
const previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = ownerStackTraceLimit;
debugStackDEV = Error('react-stack-top-frame');
Error.stackTraceLimit = previousStackTraceLimit;
} else {
debugStackDEV = unknownOwnerDebugStack;
}
}
return jsxDEVImpl(
type,
config,
maybeKey,
isStaticChildren,
debugStackDEV,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
: unknownOwnerDebugStack),
__DEV__ &&
(trackActualOwner
? createTask(getTaskName(type))
@@ -396,23 +379,15 @@ export function jsxProdSignatureRunningInDevWithStaticChildren(
const trackActualOwner =
__DEV__ &&
ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit;
let debugStackDEV = false;
if (__DEV__) {
if (trackActualOwner) {
const previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = ownerStackTraceLimit;
debugStackDEV = Error('react-stack-top-frame');
Error.stackTraceLimit = previousStackTraceLimit;
} else {
debugStackDEV = unknownOwnerDebugStack;
}
}
return jsxDEVImpl(
type,
config,
maybeKey,
isStaticChildren,
debugStackDEV,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
: unknownOwnerDebugStack),
__DEV__ &&
(trackActualOwner
? createTask(getTaskName(type))
@@ -433,23 +408,15 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren) {
const trackActualOwner =
__DEV__ &&
ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit;
let debugStackDEV = false;
if (__DEV__) {
if (trackActualOwner) {
const previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = ownerStackTraceLimit;
debugStackDEV = Error('react-stack-top-frame');
Error.stackTraceLimit = previousStackTraceLimit;
} else {
debugStackDEV = unknownOwnerDebugStack;
}
}
return jsxDEVImpl(
type,
config,
maybeKey,
isStaticChildren,
debugStackDEV,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
: unknownOwnerDebugStack),
__DEV__ &&
(trackActualOwner
? createTask(getTaskName(type))
@@ -700,23 +667,15 @@ export function createElement(type, config, children) {
const trackActualOwner =
__DEV__ &&
ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit;
let debugStackDEV = false;
if (__DEV__) {
if (trackActualOwner) {
const previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = ownerStackTraceLimit;
debugStackDEV = Error('react-stack-top-frame');
Error.stackTraceLimit = previousStackTraceLimit;
} else {
debugStackDEV = unknownOwnerDebugStack;
}
}
return ReactElement(
type,
key,
props,
getOwner(),
debugStackDEV,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
: unknownOwnerDebugStack),
__DEV__ &&
(trackActualOwner
? createTask(getTaskName(type))

View File

@@ -1,9 +1,3 @@
/**
* 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.
*/
'use strict';
const babel = require('@babel/register');

View File

@@ -1255,9 +1255,7 @@ const bundles = [
'@babel/core',
'hermes-parser',
'zod',
'zod/v4',
'zod-validation-error',
'zod-validation-error/v4',
'crypto',
'util',
],

View File

@@ -18245,12 +18245,12 @@ zip-stream@^2.1.2:
compress-commons "^2.1.1"
readable-stream "^3.4.0"
"zod-validation-error@^3.5.0 || ^4.0.0":
"zod-validation-error@^3.0.3 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
"zod@^3.25.0 || ^4.0.0":
version "4.1.12"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==
"zod@^3.22.4 || ^4.0.0":
version "4.1.11"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==