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
43 changed files with 173 additions and 488 deletions

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

@@ -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';

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

@@ -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

@@ -458,9 +458,9 @@ export function dropManualMemoization(
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`,
? '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',

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,16 +10,7 @@ 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> {
@@ -27,19 +18,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, 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') {
@@ -65,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;
}
@@ -122,73 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
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()) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'Unused useMemo()',
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',
}),
);
}
}
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;
}
}
}
}
}

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

@@ -1,35 +0,0 @@
## Input
```javascript
// @validateNoVoidUseMemo
function Component() {
useMemo(() => {
return [];
}, []);
return <div />;
}
```
## Error
```
Found 1 error:
Error: Unused useMemo()
This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
error.invalid-unused-usememo.ts:3:2
1 | // @validateNoVoidUseMemo
2 | function Component() {
> 3 | useMemo(() => {
| ^^^^^^^ useMemo() result is unused
4 | return [];
5 | }, []);
6 | return <div />;
```

View File

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

View File

@@ -28,7 +28,7 @@ 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.
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
@@ -45,7 +45,7 @@ error.useMemo-no-return-value.ts:3:16
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.
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');

View File

@@ -120,7 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
return <Child x={state} />;
}`,
errors: [makeTestCaseError('Unused useMemo()')],
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

@@ -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

@@ -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

@@ -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==