Skip to content

Mirror of upstream PR #33305 #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0e5c79c
Bruteforcing react devtools
jorge-cab May 6, 2025
8fa3dfc
Smarter Devtools integration
jorge-cab May 7, 2025
a75932b
Port relevant logic from react devtools
jorge-cab May 7, 2025
5d04d73
Add eager alternate.stateNode cleanup (#33161)
sammy-SC May 12, 2025
2bcf06b
[ReactFlightWebpackPlugin] Add support for .mjs file extension (#33028)
jennyscript May 13, 2025
b94603b
[Fizz] Gate rel="expect" behind enableFizzBlockingRender (#33183)
sebmarkbage May 13, 2025
997c7bc
[DevTools] Get source location from structured callsites in prepareSt…
sebmarkbage May 13, 2025
676f087
Reset currentEventTransitionLane after flushing sync work (#33159)
sebmarkbage May 13, 2025
0cac32d
[Fiber] Stash the entangled async action lane on currentEventTransiti…
sebmarkbage May 13, 2025
62d3f36
[Fiber] Trigger default transition indicator if needed (#33160)
sebmarkbage May 13, 2025
b480865
[Fiber] Always flush Default priority in the microtask if a Transitio…
sebmarkbage May 13, 2025
5944042
Implement Navigation API backed default indicator for DOM renderer (#…
sebmarkbage May 13, 2025
3a5b326
[Fiber] Trigger default indicator for isomorphic async actions with n…
sebmarkbage May 13, 2025
76dddd1
Port complete
jorge-cab May 13, 2025
d85f86c
Delete stray file (#33199)
kassens May 14, 2025
63d664b
Don't consider Portals animating unless they're wrapped in a ViewTran…
sebmarkbage May 14, 2025
96eb84e
Claim the useId name space for every auto named ViewTransition (#33200)
sebmarkbage May 14, 2025
3f67d08
[Fizz] Track whether we're in a fallback on FormatContext (#33194)
sebmarkbage May 15, 2025
65b5aae
[Fizz] Add vt- prefix attributes to annotate <ViewTransition> in HTML…
sebmarkbage May 15, 2025
203df2c
[compiler] Update changelog for 19.1.0-rc.2 (#33207)
poteto May 15, 2025
08cb2d7
[ci] Log author_association (#33213)
poteto May 15, 2025
4a45ba9
[sync] Fix noop for xplat (#33214)
rickhanlonii May 15, 2025
4448b18
[eslint-plugin-react-hooks] fix exhaustive deps lint rule with compon…
kassens May 15, 2025
c250b7d
[Fizz] Should be considered complete inside onShellReady callback (#3…
sebmarkbage May 16, 2025
6060367
[Fizz] Wrap revealCompletedBoundaries in a ViewTransitions aware vers…
sebmarkbage May 17, 2025
462d08f
Move SuspenseListProps into a shared/ReactTypes (#33298)
sebmarkbage May 18, 2025
94718f1
Add component tree function to devtools and finish adding componentTr…
jorge-cab May 19, 2025
2852c9d
Merge remote-tracking branch 'origin/main' into component-tree-tool
jorge-cab May 19, 2025
26315d6
Cleanup React Devtools port attempt
jorge-cab May 19, 2025
d6d929e
More cleanup
jorge-cab May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ module.exports = {
JSONValue: 'readonly',
JSResourceReference: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
React$AbstractComponent: 'readonly',
Expand Down Expand Up @@ -634,5 +635,6 @@ module.exports = {
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
navigation: 'readonly',
},
};
1 change: 1 addition & 0 deletions .github/workflows/compiler_discord_notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/runtime_commit_artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,10 @@ jobs:
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/runtime_discord_notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/shared_label_core_team_prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
Expand Down
6 changes: 6 additions & 0 deletions compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 19.1.0-rc.2 (May 14, 2025)

## babel-plugin-react-compiler

* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)

## 19.1.0-rc.1 (April 21, 2025)

## eslint-plugin-react-hooks
Expand Down

This file was deleted.

33 changes: 33 additions & 0 deletions compiler/packages/react-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
import {parseReactComponentTree} from './tools/componentTree';

function calculateMean(values: number[]): string {
return values.length > 0
Expand Down Expand Up @@ -366,6 +367,38 @@ ${calculateMean(results.renderTime)}
},
);

server.tool(
'parse-react-component-tree',
`
This tool gets the component tree of a React App.
passing in a url will attempt to connect to the browser and get the current state of the component tree. If no url is passed in,
the default url will be used (http://localhost:3000).

<requirements>
- The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
- Also the user should be running a Chrome browser running on debug mode on port 9222. If you receive an error message, advise the user to run
the following comand in the terminal:
MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
</requirements>
`,
{
url: z.string().optional().default('http://localhost:3000'),
},
async ({url}) => {
const componentTree = await parseReactComponentTree(url);

return {
content: [
{
type: 'text' as const,
text: componentTree,
},
],
};
},
);

server.prompt('review-react-code', () => ({
messages: [
{
Expand Down
38 changes: 38 additions & 0 deletions compiler/packages/react-mcp-server/src/tools/componentTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import puppeteer from 'puppeteer';

export async function parseReactComponentTree(url: string): Promise<string> {
try {
const browser = await puppeteer.connect({
browserURL: 'http://127.0.0.1:9222',
defaultViewport: null,
});

const pages = await browser.pages();

let localhostPage = null;
for (const page of pages) {
const url = await page.url();

if (url.startsWith(url)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a logical error in the URL comparison. The code is comparing url.startsWith(url), which will always evaluate to true since any string starts with itself.

This should be comparing the page's URL with the provided URL parameter:

const pageUrl = await page.url();
if (pageUrl.startsWith(url)) {
  localhostPage = page;
  break;
}

This way, the code will correctly identify pages that match the target URL.

Suggested change
if (url.startsWith(url)) {
const pageUrl = await page.url();
if (pageUrl.startsWith(url)) {

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

localhostPage = page;
break;
}
}

if (localhostPage) {
const componentTree = await localhostPage.evaluate(() => {
return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
.get(1)
.getComponentTree();
});

return componentTree;
} else {
throw new Error('Localhost page not found');
}
} catch (error) {
throw new Error(
'Failed to connect to browser, are you running chrome with --remote-debugging-port=9222?',
);
}
}
8 changes: 8 additions & 0 deletions fixtures/flight/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import {setServerState} from './ServerState.js';

async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export async function like() {
// Test loading state
await sleep(1000);
setServerState('Liked!');
return new Promise((resolve, reject) => resolve('Liked'));
}
Expand All @@ -20,5 +26,7 @@ export async function greet(formData) {
}

export async function increment(n) {
// Test loading state
await sleep(1000);
return n + 1;
}
2 changes: 2 additions & 0 deletions fixtures/view-transition/server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function render(url, res) {
const {pipe, abort} = renderToPipeableStream(
<App assets={assets} initialURL={url} />,
{
// TODO: Temporary hack. Detect from attributes instead.
bootstrapScriptContent: 'window._useVT = true;',
bootstrapScripts: [assets['main.js']],
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
Expand Down
50 changes: 36 additions & 14 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
useId,
useOptimistic,
startTransition,
Suspense,
} from 'react';

import {createPortal} from 'react-dom';
Expand All @@ -18,6 +19,10 @@ import './Page.css';

import transitions from './Transitions.module.css';

async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

const a = (
<div key="a">
<ViewTransition>
Expand Down Expand Up @@ -56,6 +61,12 @@ function Id() {
return <span id={useId()} />;
}

let wait;
function Suspend() {
if (!wait) wait = sleep(500);
return React.use(wait);
}

export default function Page({url, navigate}) {
const [renderedUrl, optimisticNavigate] = useOptimistic(
url,
Expand Down Expand Up @@ -89,7 +100,7 @@ export default function Page({url, navigate}) {
// a flushSync will.
// Promise.resolve().then(() => {
// flushSync(() => {
setCounter(c => c + 10);
// setCounter(c => c + 10);
// });
// });
}, [show]);
Expand All @@ -106,7 +117,13 @@ export default function Page({url, navigate}) {
document.body
)
) : (
<button onClick={() => startTransition(() => setShowModal(true))}>
<button
onClick={() =>
startTransition(async () => {
await sleep(2000);
setShowModal(true);
})
}>
Show Modal
</button>
);
Expand Down Expand Up @@ -183,18 +200,23 @@ export default function Page({url, navigate}) {
<div>!!</div>
</ViewTransition>
</Activity>
<p>these</p>
<p>rows</p>
<p>exist</p>
<p>to</p>
<p>test</p>
<p>scrolling</p>
<p>content</p>
<p>out</p>
<p>of</p>
{portal}
<p>the</p>
<p>viewport</p>
<Suspense fallback="Loading">
<ViewTransition>
<p>these</p>
<p>rows</p>
<p>exist</p>
<p>to</p>
<p>test</p>
<p>scrolling</p>
<p>content</p>
<p>out</p>
<p>of</p>
{portal}
<p>the</p>
<p>viewport</p>
<Suspend />
</ViewTransition>
</Suspense>
{show ? <Component /> : null}
</div>
</ViewTransition>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7746,6 +7746,34 @@ const testsFlow = {
},
],
invalid: [
{
code: normalizeIndent`
hook useExample(a) {
useEffect(() => {
console.log(a);
}, []);
}
`,
errors: [
{
message:
"React Hook useEffect has a missing dependency: 'a'. " +
'Either include it or remove the dependency array.',
suggestions: [
{
desc: 'Update the dependencies array to be: [a]',
output: normalizeIndent`
hook useExample(a) {
useEffect(() => {
console.log(a);
}, [a]);
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function Foo() {
Expand Down Expand Up @@ -8311,7 +8339,9 @@ describe('rules-of-hooks/exhaustive-deps', () => {
},
};

const testsBabelEslint = {
const testsBabelEslint = tests;

const testsHermesParser = {
valid: [...testsFlow.valid, ...tests.valid],
invalid: [...testsFlow.invalid, ...tests.invalid],
};
Expand All @@ -8336,6 +8366,33 @@ describe('rules-of-hooks/exhaustive-deps', () => {
testsBabelEslint
);

new ESLintTesterV7({
parser: require.resolve('hermes-eslint'),
parserOptions: {
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
}).run(
'eslint: v7, parser: hermes-eslint',
ReactHooksESLintRule,
testsHermesParser
);

new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('hermes-eslint'),
parserOptions: {
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
},
}).run(
'eslint: v9, parser: hermes-eslint',
ReactHooksESLintRule,
testsHermesParser
);

const testsTypescriptEslintParser = {
valid: [...testsTypescript.valid, ...tests.valid],
invalid: [...testsTypescript.invalid, ...tests.invalid],
Expand Down
Loading
Loading