Compare commits

..

23 Commits

Author SHA1 Message Date
Jorge Cabiedes
ca1d5e068b Fix flow 2025-06-02 12:56:24 -07:00
Jorge Cabiedes
ecb1861dd8 Fix lints 2025-06-02 11:16:36 -07:00
Jorge Cabiedes
a5861172c6 Gate __internal_only_getComponentTree definition 2025-06-02 11:09:42 -07:00
Jorge Cabiedes
df0a663a8c Remove unused code 2025-05-27 10:52:33 -07:00
Jorge Cabiedes
6c71a7766d Fix tests 2025-05-27 10:43:28 -07:00
Jorge Cabiedes
9275c835c3 Fix naming on react-devtools.js 2025-05-27 10:28:35 -07:00
Jorge Cabiedes
9cae1cea4e Remove getComponentTree() definition gating 2025-05-27 09:33:37 -07:00
Jorge Cabiedes
183bd4feac Fix CI 2025-05-27 09:15:24 -07:00
Jorge Cabiedes
81c3a5331e Error handling 2025-05-27 08:59:35 -07:00
Jorge Cabiedes
ab86a5efe8 Address comments 2025-05-27 08:54:30 -07:00
Jorge Cabiedes
1e4614bf13 Add IS_INTERNAL flag to eslintrc 2025-05-20 10:50:49 -07:00
Jorge Cabiedes
c5ab27a649 Add IS_INTERNAL build time flag and gate getComponentTree() 2025-05-20 10:44:16 -07:00
Jorge Cabiedes
049bfbb169 Add IS_INTERNAL build time flag and gate getComponentTree() 2025-05-20 10:43:14 -07:00
Jorge Cabiedes
a85b0b0bb4 Error handling 2025-05-19 11:27:45 -07:00
Jorge Cabiedes
789e5f02c5 fix url redefinition 2025-05-19 11:16:40 -07:00
Jorge Cabiedes
d6d929e2f1 More cleanup 2025-05-19 09:11:40 -07:00
Jorge Cabiedes
26315d64dc Cleanup React Devtools port attempt 2025-05-19 09:09:19 -07:00
Jorge Cabiedes
2852c9d08c Merge remote-tracking branch 'origin/main' into component-tree-tool 2025-05-19 09:03:55 -07:00
Jorge Cabiedes
94718f18b4 Add component tree function to devtools and finish adding componentTree mcp tool 2025-05-19 09:00:49 -07:00
Jorge Cabiedes Acosta
76dddd1d57 Port complete 2025-05-13 16:05:41 -07:00
Jorge Cabiedes
a75932b2ea Port relevant logic from react devtools 2025-05-07 16:37:34 -07:00
Jorge Cabiedes Acosta
8fa3dfc845 Smarter Devtools integration 2025-05-07 14:39:04 -07:00
Jorge Cabiedes Acosta
0e5c79cfea Bruteforcing react devtools 2025-05-06 08:21:12 -07:00
30 changed files with 225 additions and 962 deletions

View File

@@ -496,6 +496,7 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -504,6 +505,7 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},

View File

@@ -452,7 +452,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
description: `Cannot call ${hookKind} within a function component`,
suggestions: null,
}),
);

View File

@@ -23,7 +23,7 @@ const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
6 | const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
7 | useEffect(() => {
> 8 | useHookInsideCallback();
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
9 | });
10 | return <button {...props} ref={ref} />;
11 | });

View File

@@ -23,7 +23,7 @@ const ComponentWithHookInsideCallback = React.memo(props => {
6 | const ComponentWithHookInsideCallback = React.memo(props => {
7 | useEffect(() => {
> 8 | useHookInsideCallback();
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
9 | });
10 | return <button {...props} />;
11 | });

View File

@@ -31,7 +31,7 @@ function Component() {
8 | const y = {
9 | inner() {
> 10 | return useFoo();
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (10:10)
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (10:10)
11 | },
12 | };
13 | return y;

View File

@@ -27,7 +27,7 @@ function Component() {
6 | const y = {
7 | inner() {
> 8 | return useFoo();
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (8:8)
| ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
9 | },
10 | };
11 | return y;

View File

@@ -21,7 +21,7 @@ function createHook() {
4 | return function useHookWithConditionalHook() {
5 | if (cond) {
> 6 | useConditionalHook();
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
7 | }
8 | };
9 | }

View File

@@ -21,9 +21,9 @@ function createComponent() {
4 | return function ComponentWithHookInsideCallback() {
5 | useEffect(() => {
> 6 | useHookInsideCallback();
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function expression (5:5)
InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (5:5)
7 | });
8 | };
9 | }

View File

@@ -21,7 +21,7 @@ function createComponent() {
4 | return function ComponentWithHookInsideCallback() {
5 | function handleClick() {
> 6 | useState();
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function expression (6:6)
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function component (6:6)
7 | }
8 | };
9 | }

View File

@@ -19,7 +19,7 @@ function ComponentWithHookInsideCallback() {
3 | function ComponentWithHookInsideCallback() {
4 | function handleClick() {
> 5 | useState();
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function expression (5:5)
| ^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useState within a function component (5:5)
6 | }
7 | }
8 |

View File

@@ -21,7 +21,7 @@ function createComponent() {
4 | return function ComponentWithConditionalHook() {
5 | if (cond) {
> 6 | useConditionalHook();
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (6:6)
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (6:6)
7 | }
8 | };
9 | }

View File

@@ -19,7 +19,7 @@ function ComponentWithHookInsideCallback() {
3 | function ComponentWithHookInsideCallback() {
4 | useEffect(() => {
> 5 | useHookInsideCallback();
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call hook within a function expression (5:5)
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (5:5)
6 | });
7 | }
8 |

View File

@@ -31,7 +31,7 @@ function Component(props) {
7 | };
8 | useEffect(() => {
> 9 | useEffect(() => {
| ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function expression (9:9)
| ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9)
10 | function nested() {
11 | fire(foo(props));
12 | }

View File

@@ -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
@@ -366,6 +367,45 @@ ${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}) => {
try {
const componentTree = await parseReactComponentTree(url);
return {
content: [
{
type: 'text' as const,
text: componentTree,
},
],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
}
},
);
server.prompt('review-react-code', () => ({
messages: [
{

View File

@@ -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 pageUrl = await page.url();
if (pageUrl.startsWith(url)) {
localhostPage = page;
break;
}
}
if (localhostPage) {
const componentTree = await localhostPage.evaluate(() => {
return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
.get(1)
.__internal_only_getComponentTree();
});
return componentTree;
} else {
throw new Error(
`Could not open the page at ${url}. Is your server running?`,
);
}
} catch (error) {
throw new Error('Failed extract component tree' + error);
}
}

View File

@@ -1,12 +1,8 @@
import React, {
Fragment,
Suspense,
unstable_SuspenseList as SuspenseList,
} from 'react';
import React, {Fragment, Suspense} from 'react';
export default function LargeContent() {
return (
<SuspenseList revealOrder="forwards">
<Fragment>
<Suspense fallback={null}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
@@ -290,6 +286,6 @@ export default function LargeContent() {
interdum a. Proin nec odio in nulla vestibulum.
</p>
</Suspense>
</SuspenseList>
</Fragment>
);
}

View File

@@ -515,22 +515,6 @@ const tests = {
`,
options: [{additionalHooks: 'useCustomEffect'}],
},
{
// behaves like no deps
code: normalizeIndent`
function MyComponent(props) {
useSpecialEffect(() => {
console.log(props.foo);
}, null);
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
},
{
code: normalizeIndent`
function MyComponent(props) {
@@ -1486,38 +1470,6 @@ const tests = {
},
],
invalid: [
{
code: normalizeIndent`
function MyComponent(props) {
useSpecialEffect(() => {
console.log(props.foo);
}, null);
}
`,
options: [{additionalHooks: 'useSpecialEffect'}],
errors: [
{
message:
"React Hook useSpecialEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.",
},
{
message:
"React Hook useSpecialEffect has a missing dependency: 'props.foo'. Either include it or remove the dependency array.",
suggestions: [
{
desc: 'Update the dependencies array to be: [props.foo]',
output: normalizeIndent`
function MyComponent(props) {
useSpecialEffect(() => {
console.log(props.foo);
}, [props.foo]);
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function MyComponent(props) {
@@ -7869,24 +7821,6 @@ const testsTypescript = {
}
`,
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber);
})
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
},
{
code: normalizeIndent`
function App() {
@@ -8242,48 +8176,6 @@ const testsTypescript = {
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber + state);
}, [])
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
errors: [
{
message:
"React Hook useSpecialEffect has a missing dependency: 'state'. " +
'Either include it or remove the dependency array. ' +
`You can also do a functional update 'setState(s => ...)' ` +
`if you only need 'state' in the 'setState' call.`,
suggestions: [
{
desc: 'Update the dependencies array to be: [state]',
output: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber + state);
}, [state])
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useMemo(() => {
const someNumber: typeof state = 2;
console.log(someNumber);

View File

@@ -61,38 +61,27 @@ const rule = {
enableDangerousAutofixThisMayCauseInfiniteLoops: {
type: 'boolean',
},
experimental_autoDependenciesHooks: {
type: 'array',
items: {
type: 'string',
},
},
},
},
],
},
create(context: Rule.RuleContext) {
const rawOptions = context.options && context.options[0];
// Parse the `additionalHooks` regex.
const additionalHooks =
rawOptions && rawOptions.additionalHooks
? new RegExp(rawOptions.additionalHooks)
context.options &&
context.options[0] &&
context.options[0].additionalHooks
? new RegExp(context.options[0].additionalHooks)
: undefined;
const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean =
(rawOptions &&
rawOptions.enableDangerousAutofixThisMayCauseInfiniteLoops) ||
(context.options &&
context.options[0] &&
context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
false;
const experimental_autoDependenciesHooks: ReadonlyArray<string> =
rawOptions && Array.isArray(rawOptions.experimental_autoDependenciesHooks)
? rawOptions.experimental_autoDependenciesHooks
: [];
const options = {
additionalHooks,
experimental_autoDependenciesHooks,
enableDangerousAutofixThisMayCauseInfiniteLoops,
};
@@ -173,7 +162,6 @@ const rule = {
reactiveHook: Node,
reactiveHookName: string,
isEffect: boolean,
isAutoDepsHook: boolean,
): void {
if (isEffect && node.async) {
reportProblem({
@@ -661,9 +649,6 @@ const rule = {
}
if (!declaredDependenciesNode) {
if (isAutoDepsHook) {
return;
}
// Check if there are any top-level setState() calls.
// Those tend to lead to infinite loops.
let setStateInsideEffectWithoutDeps: string | null = null;
@@ -726,13 +711,6 @@ const rule = {
}
return;
}
if (
isAutoDepsHook &&
declaredDependenciesNode.type === 'Literal' &&
declaredDependenciesNode.value === null
) {
return;
}
const declaredDependencies: Array<DeclaredDependency> = [];
const externalDependencies = new Set<string>();
@@ -1340,19 +1318,10 @@ const rule = {
return;
}
const isAutoDepsHook =
options.experimental_autoDependenciesHooks.includes(reactiveHookName);
// Check the declared dependencies for this reactive hook. If there is no
// second argument then the reactive callback will re-run on every render.
// So no need to check for dependency inclusion.
if (
(!declaredDependenciesNode ||
(isAutoDepsHook &&
declaredDependenciesNode.type === 'Literal' &&
declaredDependenciesNode.value === null)) &&
!isEffect
) {
if (!declaredDependenciesNode && !isEffect) {
// These are only used for optimization.
if (
reactiveHookName === 'useMemo' ||
@@ -1386,17 +1355,11 @@ const rule = {
reactiveHook,
reactiveHookName,
isEffect,
isAutoDepsHook,
);
return; // Handled
case 'Identifier':
if (
!declaredDependenciesNode ||
(isAutoDepsHook &&
declaredDependenciesNode.type === 'Literal' &&
declaredDependenciesNode.value === null)
) {
// Always runs, no problems.
if (!declaredDependenciesNode) {
// No deps, no problems.
return; // Handled
}
// The function passed as a callback is not written inline.
@@ -1445,7 +1408,6 @@ const rule = {
reactiveHook,
reactiveHookName,
isEffect,
isAutoDepsHook,
);
return; // Handled
case 'VariableDeclarator':
@@ -1465,7 +1427,6 @@ const rule = {
reactiveHook,
reactiveHookName,
isEffect,
isAutoDepsHook,
);
return; // Handled
}

View File

@@ -72,6 +72,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_EDGE__: false,
__IS_NATIVE__: true,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,

View File

@@ -91,6 +91,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_CHROME__: false,
__IS_EDGE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
}),
new Webpack.SourceMapDevToolPlugin({
filename: '[file].map',

View File

@@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
const IS_EDGE = process.env.IS_EDGE === 'true';
const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';
const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
const babelOptions = {
@@ -113,6 +115,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
__IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD,
__IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,

View File

@@ -86,6 +86,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_FIREFOX__: false,
__IS_EDGE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_EDGE__: false,
__IS_NATIVE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@@ -5859,6 +5859,86 @@ export function attach(
return unresolvedSource;
}
type InternalMcpFunctions = {
__internal_only_getComponentTree?: Function,
};
const internalMcpFunctions: InternalMcpFunctions = {};
if (__IS_INTERNAL_MCP_BUILD__) {
// eslint-disable-next-line no-inner-declarations
function __internal_only_getComponentTree(): string {
let treeString = '';
function buildTreeString(
instance: DevToolsInstance,
prefix: string = '',
isLastChild: boolean = true,
): void {
if (!instance) return;
const name =
(instance.kind !== VIRTUAL_INSTANCE
? getDisplayNameForFiber(instance.data)
: instance.data.name) || 'Unknown';
const id = instance.id !== undefined ? instance.id : 'unknown';
if (name !== 'createRoot()') {
treeString +=
prefix +
(isLastChild ? '└── ' : '├── ') +
name +
' (id: ' +
id +
')\n';
}
const childPrefix = prefix + (isLastChild ? ' ' : '│ ');
let childCount = 0;
let tempChild = instance.firstChild;
while (tempChild !== null) {
childCount++;
tempChild = tempChild.nextSibling;
}
let child = instance.firstChild;
let currentChildIndex = 0;
while (child !== null) {
currentChildIndex++;
const isLastSibling = currentChildIndex === childCount;
buildTreeString(child, childPrefix, isLastSibling);
child = child.nextSibling;
}
}
const rootInstances: Array<DevToolsInstance> = [];
idToDevToolsInstanceMap.forEach(instance => {
if (instance.parent === null || instance.parent.parent === null) {
rootInstances.push(instance);
}
});
if (rootInstances.length > 0) {
for (let i = 0; i < rootInstances.length; i++) {
const isLast = i === rootInstances.length - 1;
buildTreeString(rootInstances[i], '', isLast);
if (!isLast) {
treeString += '\n';
}
}
} else {
treeString = 'No component tree found.';
}
return treeString;
}
internalMcpFunctions.__internal_only_getComponentTree =
__internal_only_getComponentTree;
}
return {
cleanup,
clearErrorsAndWarnings,
@@ -5898,5 +5978,6 @@ export function attach(
storeAsGlobal,
updateComponentFilters,
getEnvironmentNames,
...internalMcpFunctions,
};
}

View File

@@ -1318,8 +1318,10 @@ describe('ReactDOMFizzServer', () => {
expect(ref.current).toBe(null);
expect(getVisibleChildren(container)).toEqual(
<div>
{'Loading A'}
{'Loading B'}
Loading A
{/* // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
// isn't implemented fully yet. */}
<span>B</span>
</div>,
);
@@ -1333,9 +1335,11 @@ describe('ReactDOMFizzServer', () => {
// We haven't resolved yet.
expect(getVisibleChildren(container)).toEqual(
<div>
{'Loading A'}
{'Loading B'}
{'Loading C'}
Loading A
{/* // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
// isn't implemented fully yet. */}
<span>B</span>
Loading C
</div>,
);

View File

@@ -1,327 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/
'use strict';
import {
insertNodesAndExecuteScripts,
getVisibleChildren,
} from '../test-utils/FizzTestUtils';
let JSDOM;
let React;
let Suspense;
let SuspenseList;
let assertLog;
let Scheduler;
let ReactDOMFizzServer;
let Stream;
let document;
let writable;
let container;
let buffer = '';
let hasErrored = false;
let fatalError = undefined;
describe('ReactDOMFizSuspenseList', () => {
beforeEach(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
React = require('react');
assertLog = require('internal-test-utils').assertLog;
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
SuspenseList = React.unstable_SuspenseList;
Scheduler = require('scheduler');
// Test Environment
const jsdom = new JSDOM(
'<!DOCTYPE html><html><head></head><body><div id="container">',
{
runScripts: 'dangerously',
},
);
document = jsdom.window.document;
container = document.getElementById('container');
global.window = jsdom.window;
// The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.
global.requestAnimationFrame = global.window.requestAnimationFrame = cb =>
setTimeout(cb);
buffer = '';
hasErrored = false;
writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
hasErrored = true;
fatalError = error;
});
});
afterEach(() => {
jest.restoreAllMocks();
});
async function serverAct(callback) {
await callback();
// Await one turn around the event loop.
// This assumes that we'll flush everything we have so far.
await new Promise(resolve => {
setImmediate(resolve);
});
if (hasErrored) {
throw fatalError;
}
// JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
// We also want to execute any scripts that are embedded.
// We assume that we have now received a proper fragment of HTML.
const bufferedContent = buffer;
buffer = '';
const temp = document.createElement('body');
temp.innerHTML = bufferedContent;
await insertNodesAndExecuteScripts(temp, container, null);
jest.runAllTimers();
}
function Text(props) {
Scheduler.log(props.text);
return <span>{props.text}</span>;
}
function createAsyncText(text) {
let resolved = false;
const Component = function () {
if (!resolved) {
Scheduler.log('Suspend! [' + text + ']');
throw promise;
}
return <Text text={text} />;
};
const promise = new Promise(resolve => {
Component.resolve = function () {
resolved = true;
return resolve();
};
});
return Component;
}
// @gate enableSuspenseList
it('shows content independently by default', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
const C = createAsyncText('C');
function Foo() {
return (
<div>
<SuspenseList>
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}
await A.resolve();
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);
await serverAct(() => C.resolve());
assertLog(['C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>C</span>
</div>,
);
await serverAct(() => B.resolve());
assertLog(['B']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
// @gate enableSuspenseList
it('displays each items in "forwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
const C = createAsyncText('C');
function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}
await C.resolve();
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
assertLog([
'Suspend! [A]',
'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
'C',
'Loading A',
'Loading B',
'Loading C',
]);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);
await serverAct(() => A.resolve());
assertLog(['A']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);
await serverAct(() => B.resolve());
assertLog(['B']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
// @gate enableSuspenseList
it('displays each items in "backwards" order', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
const C = createAsyncText('C');
function Foo() {
return (
<div>
<SuspenseList revealOrder="backwards">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}
await A.resolve();
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
assertLog([
'Suspend! [C]',
'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
'A',
'Loading C',
'Loading B',
'Loading A',
]);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);
await serverAct(() => C.resolve());
assertLog(['C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>C</span>
</div>,
);
await serverAct(() => B.resolve());
assertLog(['B']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
});

View File

@@ -24,8 +24,6 @@ import type {
ViewTransitionProps,
ActivityProps,
SuspenseProps,
SuspenseListProps,
SuspenseListRevealOrder,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
@@ -233,12 +231,6 @@ type LegacyContext = {
[key: string]: any,
};
type SuspenseListRow = {
pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.
boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)
next: null | SuspenseListRow, // The next row blocked by this one.
};
const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
type SuspenseBoundary = {
@@ -246,7 +238,6 @@ type SuspenseBoundary = {
rootSegmentID: number,
parentFlushed: boolean,
pendingTasks: number, // when it reaches zero we can show this boundary's content
row: null | SuspenseListRow, // the row that this boundary blocks from completing.
completedSegments: Array<Segment>, // completed but not yet flushed segments.
byteSize: number, // used to determine whether to inline children boundaries.
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
@@ -277,12 +268,11 @@ type RenderTask = {
formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside
componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
thenableState: null | ThenableState,
legacyContext: LegacyContext, // the current legacy context that this task is executing in
debugTask: null | ConsoleTask, // DEV only
// DON'T ANY MORE FIELDS. We at 16 in prod already which otherwise requires converting to a constructor.
// DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
// Consider splitting into multiple objects or consolidating some fields.
};
@@ -308,11 +298,12 @@ type ReplayTask = {
formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside
componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
thenableState: null | ThenableState,
legacyContext: LegacyContext, // the current legacy context that this task is executing in
debugTask: null | ConsoleTask, // DEV only
// DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
// Consider splitting into multiple objects or consolidating some fields.
};
export type Task = RenderTask | ReplayTask;
@@ -551,7 +542,6 @@ export function createRequest(
rootContextSnapshot,
emptyTreeContext,
null,
null,
emptyContextObject,
null,
);
@@ -657,7 +647,6 @@ export function resumeRequest(
rootContextSnapshot,
emptyTreeContext,
null,
null,
emptyContextObject,
null,
);
@@ -685,7 +674,6 @@ export function resumeRequest(
rootContextSnapshot,
emptyTreeContext,
null,
null,
emptyContextObject,
null,
);
@@ -751,7 +739,6 @@ function pingTask(request: Request, task: Task): void {
function createSuspenseBoundary(
request: Request,
row: null | SuspenseListRow,
fallbackAbortableTasks: Set<Task>,
contentPreamble: null | Preamble,
fallbackPreamble: null | Preamble,
@@ -761,7 +748,6 @@ function createSuspenseBoundary(
rootSegmentID: -1,
parentFlushed: false,
pendingTasks: 0,
row: row,
completedSegments: [],
byteSize: 0,
fallbackAbortableTasks,
@@ -779,17 +765,6 @@ function createSuspenseBoundary(
boundary.errorStack = null;
boundary.errorComponentStack = null;
}
if (row !== null) {
// This boundary will block this row from completing.
row.pendingTasks++;
const blockedBoundaries = row.boundaries;
if (blockedBoundaries !== null) {
// Previous rows will block this boundary itself from completing.
request.allPendingTasks++;
boundary.pendingTasks++;
blockedBoundaries.push(boundary);
}
}
return boundary;
}
@@ -807,7 +782,6 @@ function createRenderTask(
formatContext: FormatContext,
context: ContextSnapshot,
treeContext: TreeContext,
row: null | SuspenseListRow,
componentStack: null | ComponentStackNode,
legacyContext: LegacyContext,
debugTask: null | ConsoleTask,
@@ -818,9 +792,6 @@ function createRenderTask(
} else {
blockedBoundary.pendingTasks++;
}
if (row !== null) {
row.pendingTasks++;
}
const task: RenderTask = ({
replay: null,
node,
@@ -835,7 +806,6 @@ function createRenderTask(
formatContext,
context,
treeContext,
row,
componentStack,
thenableState,
}: any);
@@ -862,7 +832,6 @@ function createReplayTask(
formatContext: FormatContext,
context: ContextSnapshot,
treeContext: TreeContext,
row: null | SuspenseListRow,
componentStack: null | ComponentStackNode,
legacyContext: LegacyContext,
debugTask: null | ConsoleTask,
@@ -873,9 +842,6 @@ function createReplayTask(
} else {
blockedBoundary.pendingTasks++;
}
if (row !== null) {
row.pendingTasks++;
}
replay.pendingTasks++;
const task: ReplayTask = ({
replay,
@@ -891,7 +857,6 @@ function createReplayTask(
formatContext,
context,
treeContext,
row,
componentStack,
thenableState,
}: any);
@@ -1180,20 +1145,17 @@ function renderSuspenseBoundary(
// so we can just render through it.
const prevKeyPath = someTask.keyPath;
const prevContext = someTask.formatContext;
const prevRow = someTask.row;
someTask.keyPath = keyPath;
someTask.formatContext = getSuspenseContentFormatContext(
request.resumableState,
prevContext,
);
someTask.row = null;
const content: ReactNodeList = props.children;
try {
renderNode(request, someTask, content, -1);
} finally {
someTask.keyPath = prevKeyPath;
someTask.formatContext = prevContext;
someTask.row = prevRow;
}
return;
}
@@ -1202,7 +1164,6 @@ function renderSuspenseBoundary(
const prevKeyPath = task.keyPath;
const prevContext = task.formatContext;
const prevRow = task.row;
const parentBoundary = task.blockedBoundary;
const parentPreamble = task.blockedPreamble;
const parentHoistableState = task.hoistableState;
@@ -1220,19 +1181,12 @@ function renderSuspenseBoundary(
if (canHavePreamble(task.formatContext)) {
newBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
createPreambleState(),
createPreambleState(),
);
} else {
newBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
null,
null,
);
newBoundary = createSuspenseBoundary(request, fallbackAbortSet, null, null);
}
if (request.trackedPostpones !== null) {
newBoundary.trackedContentKeyPath = keyPath;
@@ -1336,7 +1290,6 @@ function renderSuspenseBoundary(
),
task.context,
task.treeContext,
null, // The row gets reset inside the Suspense boundary.
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -1365,7 +1318,6 @@ function renderSuspenseBoundary(
request.resumableState,
prevContext,
);
task.row = null;
contentRootSegment.status = RENDERING;
try {
@@ -1387,14 +1339,6 @@ function renderSuspenseBoundary(
// the fallback. However, if this boundary ended up big enough to be eligible for outlining
// we can't do that because we might still need the fallback if we outline it.
if (!isEligibleForOutlining(request, newBoundary)) {
if (prevRow !== null) {
// If we have synchronously completed the boundary and it's not eligible for outlining
// then we don't have to wait for it to be flushed before we unblock future rows.
// This lets us inline small rows in order.
if (--prevRow.pendingTasks === 0) {
finishSuspenseListRow(request, prevRow);
}
}
if (request.pendingRootTasks === 0 && task.blockedPreamble) {
// The root is complete and this boundary may contribute part of the preamble.
// We eagerly attempt to prepare the preamble here because we expect most requests
@@ -1461,7 +1405,6 @@ function renderSuspenseBoundary(
task.blockedSegment = parentSegment;
task.keyPath = prevKeyPath;
task.formatContext = prevContext;
task.row = prevRow;
}
const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1484,7 +1427,6 @@ function renderSuspenseBoundary(
),
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -1509,7 +1451,6 @@ function replaySuspenseBoundary(
): void {
const prevKeyPath = task.keyPath;
const prevContext = task.formatContext;
const prevRow = task.row;
const previousReplaySet: ReplaySet = task.replay;
const parentBoundary = task.blockedBoundary;
@@ -1523,7 +1464,6 @@ function replaySuspenseBoundary(
if (canHavePreamble(task.formatContext)) {
resumedBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
createPreambleState(),
createPreambleState(),
@@ -1531,7 +1471,6 @@ function replaySuspenseBoundary(
} else {
resumedBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
null,
null,
@@ -1551,7 +1490,6 @@ function replaySuspenseBoundary(
request.resumableState,
prevContext,
);
task.row = null;
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
try {
@@ -1628,7 +1566,6 @@ function replaySuspenseBoundary(
task.replay = previousReplaySet;
task.keyPath = prevKeyPath;
task.formatContext = prevContext;
task.row = prevRow;
}
const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1656,7 +1593,6 @@ function replaySuspenseBoundary(
),
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -1668,317 +1604,6 @@ function replaySuspenseBoundary(
request.pingedTasks.push(suspendedFallbackTask);
}
function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {
// This row finished. Now we have to unblock all the next rows that were blocked on this.
// We do this in a loop to avoid stack overflow for very long lists that get unblocked.
let unblockedRow = row.next;
while (unblockedRow !== null) {
// Unblocking the boundaries will decrement the count of this row but we keep it above
// zero so they never finish this row recursively.
const unblockedBoundaries = unblockedRow.boundaries;
if (unblockedBoundaries !== null) {
unblockedRow.boundaries = null;
for (let i = 0; i < unblockedBoundaries.length; i++) {
finishedTask(request, unblockedBoundaries[i], null, null);
}
}
// Instead we decrement at the end to keep it all in this loop.
unblockedRow.pendingTasks--;
if (unblockedRow.pendingTasks > 0) {
// Still blocked.
break;
}
unblockedRow = unblockedRow.next;
}
}
function createSuspenseListRow(
previousRow: null | SuspenseListRow,
): SuspenseListRow {
const newRow: SuspenseListRow = {
pendingTasks: 1, // At first the row is blocked on attempting rendering itself.
boundaries: null,
next: null,
};
if (previousRow !== null && previousRow.pendingTasks > 0) {
// If the previous row is not done yet, we add ourselves to be blocked on it.
// When it finishes, we'll decrement our pending tasks.
newRow.pendingTasks++;
newRow.boundaries = [];
previousRow.next = newRow;
}
return newRow;
}
function renderSuspenseListRows(
request: Request,
task: Task,
keyPath: KeyNode,
rows: Array<ReactNodeList>,
revealOrder: 'forwards' | 'backwards',
): void {
// This is a fork of renderChildrenArray that's aware of tracking rows.
const prevKeyPath = task.keyPath;
const previousComponentStack = task.componentStack;
let previousDebugTask = null;
if (__DEV__) {
previousDebugTask = task.debugTask;
// We read debugInfo from task.node.props.children instead of rows because it
// might have been an unwrapped iterable so we read from the original node.
pushServerComponentStack(task, (task.node: any).props.children._debugInfo);
}
const prevTreeContext = task.treeContext;
const prevRow = task.row;
const totalChildren = rows.length;
if (task.replay !== null) {
// Replay
// First we need to check if we have any resume slots at this level.
const resumeSlots = task.replay.slots;
if (resumeSlots !== null && typeof resumeSlots === 'object') {
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let n = 0; n < totalChildren; n++) {
// Since we are going to resume into a slot whose order was already
// determined by the prerender, we can safely resume it even in reverse
// render order.
const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n;
const node = rows[i];
task.row = previousSuspenseListRow = createSuspenseListRow(
previousSuspenseListRow,
);
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
const resumeSegmentID = resumeSlots[i];
// TODO: If this errors we should still continue with the next sibling.
if (typeof resumeSegmentID === 'number') {
resumeNode(request, task, resumeSegmentID, node, i);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
delete resumeSlots[i];
} else {
renderNode(request, task, node, i);
}
if (--previousSuspenseListRow.pendingTasks === 0) {
finishSuspenseListRow(request, previousSuspenseListRow);
}
}
} else {
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let n = 0; n < totalChildren; n++) {
// Since we are going to resume into a slot whose order was already
// determined by the prerender, we can safely resume it even in reverse
// render order.
const i = revealOrder !== 'backwards' ? n : totalChildren - 1 - n;
const node = rows[i];
if (__DEV__) {
warnForMissingKey(request, task, node);
}
task.row = previousSuspenseListRow = createSuspenseListRow(
previousSuspenseListRow,
);
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
renderNode(request, task, node, i);
if (--previousSuspenseListRow.pendingTasks === 0) {
finishSuspenseListRow(request, previousSuspenseListRow);
}
}
}
} else {
task = ((task: any): RenderTask); // Refined
if (revealOrder !== 'backwards') {
// Forwards direction
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let i = 0; i < totalChildren; i++) {
const node = rows[i];
if (__DEV__) {
warnForMissingKey(request, task, node);
}
task.row = previousSuspenseListRow = createSuspenseListRow(
previousSuspenseListRow,
);
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
renderNode(request, task, node, i);
if (--previousSuspenseListRow.pendingTasks === 0) {
finishSuspenseListRow(request, previousSuspenseListRow);
}
}
} else {
// For backwards direction we need to do things a bit differently.
// We give each row its own segment so that we can render the content in
// reverse order but still emit it in the right order when we flush.
const parentSegment = task.blockedSegment;
const childIndex = parentSegment.children.length;
const insertionIndex = parentSegment.chunks.length;
let previousSuspenseListRow: null | SuspenseListRow = null;
for (let i = totalChildren - 1; i >= 0; i--) {
const node = rows[i];
task.row = previousSuspenseListRow = createSuspenseListRow(
previousSuspenseListRow,
);
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
const newSegment = createPendingSegment(
request,
insertionIndex,
null,
task.formatContext,
// Assume we are text embedded at the trailing edges
i === 0 ? parentSegment.lastPushedText : true,
true,
);
// Insert in the beginning of the sequence, which will insert before any previous rows.
parentSegment.children.splice(childIndex, 0, newSegment);
task.blockedSegment = newSegment;
if (__DEV__) {
warnForMissingKey(request, task, node);
}
try {
renderNode(request, task, node, i);
pushSegmentFinale(
newSegment.chunks,
request.renderState,
newSegment.lastPushedText,
newSegment.textEmbedded,
);
newSegment.status = COMPLETED;
finishedSegment(request, task.blockedBoundary, newSegment);
if (--previousSuspenseListRow.pendingTasks === 0) {
finishSuspenseListRow(request, previousSuspenseListRow);
}
} catch (thrownValue: mixed) {
if (request.status === ABORTING) {
newSegment.status = ABORTED;
} else {
newSegment.status = ERRORED;
}
throw thrownValue;
}
}
task.blockedSegment = parentSegment;
// Reset lastPushedText for current Segment since the new Segments "consumed" it
parentSegment.lastPushedText = false;
}
}
// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
task.treeContext = prevTreeContext;
task.row = prevRow;
task.keyPath = prevKeyPath;
if (__DEV__) {
task.componentStack = previousComponentStack;
task.debugTask = previousDebugTask;
}
}
function renderSuspenseList(
request: Request,
task: Task,
keyPath: KeyNode,
props: SuspenseListProps,
): void {
const children: any = props.children;
const revealOrder: SuspenseListRevealOrder = props.revealOrder;
// TODO: Support tail hidden/collapsed modes.
// const tailMode: SuspenseListTailMode = props.tail;
if (revealOrder === 'forwards' || revealOrder === 'backwards') {
// For ordered reveal, we need to produce rows from the children.
if (isArray(children)) {
renderSuspenseListRows(request, task, keyPath, children, revealOrder);
return;
}
const iteratorFn = getIteratorFn(children);
if (iteratorFn) {
const iterator = iteratorFn.call(children);
if (iterator) {
if (__DEV__) {
validateIterable(task, children, -1, iterator, iteratorFn);
}
// TODO: We currently use the same id algorithm as regular nodes
// but we need a new algorithm for SuspenseList that doesn't require
// a full set to be loaded up front to support Async Iterable.
// When we have that, we shouldn't buffer anymore.
let step = iterator.next();
if (!step.done) {
const rows = [];
do {
rows.push(step.value);
step = iterator.next();
} while (!step.done);
renderSuspenseListRows(request, task, keyPath, children, revealOrder);
}
return;
}
}
if (
enableAsyncIterableChildren &&
typeof (children: any)[ASYNC_ITERATOR] === 'function'
) {
const iterator: AsyncIterator<ReactNodeList> = (children: any)[
ASYNC_ITERATOR
]();
if (iterator) {
if (__DEV__) {
validateAsyncIterable(task, (children: any), -1, iterator);
}
// TODO: Update the task.children to be the iterator to avoid asking
// for new iterators, but we currently warn for rendering these
// so needs some refactoring to deal with the warning.
// Restore the thenable state before resuming.
const prevThenableState = task.thenableState;
task.thenableState = null;
prepareToUseThenableState(prevThenableState);
// We need to know how many total rows are in this set, so that we
// can allocate enough id slots to acommodate them. So we must exhaust
// the iterator before we start recursively rendering the rows.
// TODO: This is not great but I think it's inherent to the id
// generation algorithm.
const rows = [];
let done = false;
if (iterator === children) {
// If it's an iterator we need to continue reading where we left
// off. We can do that by reading the first few rows from the previous
// thenable state.
// $FlowFixMe
let step = readPreviousThenableFromState();
while (step !== undefined) {
if (step.done) {
done = true;
break;
}
rows.push(step.value);
step = readPreviousThenableFromState();
}
}
if (!done) {
let step = unwrapThenable(iterator.next());
while (!step.done) {
rows.push(step.value);
step = unwrapThenable(iterator.next());
}
}
renderSuspenseListRows(request, task, keyPath, rows, revealOrder);
return;
}
}
// This case will warn on the client. It's the same as independent revealOrder.
}
if (revealOrder === 'together') {
// TODO
}
// For other reveal order modes, we just render it as a fragment.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, children, -1);
task.keyPath = prevKeyPath;
}
function renderPreamble(
request: Request,
task: Task,
@@ -2009,7 +1634,6 @@ function renderPreamble(
task.formatContext,
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -2759,7 +2383,11 @@ function renderElement(
return;
}
case REACT_SUSPENSE_LIST_TYPE: {
renderSuspenseList(request, task, keyPath, props);
// TODO: SuspenseList should control the boundaries.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, props.children, -1);
task.keyPath = prevKeyPath;
return;
}
case REACT_VIEW_TRANSITION_TYPE: {
@@ -3909,7 +3537,6 @@ function spawnNewSuspendedReplayTask(
task.formatContext,
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -3951,7 +3578,6 @@ function spawnNewSuspendedRenderTask(
task.formatContext,
task.context,
task.treeContext,
task.row,
task.componentStack,
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ ? task.debugTask : null,
@@ -4260,19 +3886,10 @@ function erroredReplay(
function erroredTask(
request: Request,
boundary: Root | SuspenseBoundary,
row: null | SuspenseListRow,
error: mixed,
errorInfo: ThrownInfo,
debugTask: null | ConsoleTask,
) {
if (row !== null) {
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
request.allPendingTasks--;
// Report the error to a global handler.
let errorDigest;
// We don't handle halts here because we only halt when prerendering and
@@ -4324,6 +3941,7 @@ function erroredTask(
}
}
request.allPendingTasks--;
if (request.allPendingTasks === 0) {
completeAll(request);
}
@@ -4338,7 +3956,7 @@ function abortTaskSoft(this: Request, task: Task): void {
const segment = task.blockedSegment;
if (segment !== null) {
segment.status = ABORTED;
finishedTask(request, boundary, task.row, segment);
finishedTask(request, boundary, segment);
}
}
@@ -4352,7 +3970,6 @@ function abortRemainingSuspenseBoundary(
): void {
const resumedBoundary = createSuspenseBoundary(
request,
null,
new Set(),
null,
null,
@@ -4452,13 +4069,6 @@ function abortTask(task: Task, request: Request, error: mixed): void {
segment.status = ABORTED;
}
const row = task.row;
if (row !== null) {
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
const errorInfo = getThrownInfo(task.componentStack);
if (boundary === null) {
@@ -4481,7 +4091,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
// we just need to mark it as postponed.
logPostpone(request, postponeInstance.message, errorInfo, null);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, null, row, segment);
finishedTask(request, null, segment);
} else {
const fatal = new Error(
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
@@ -4500,7 +4110,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
// We log the error but we still resolve the prerender
logRecoverableError(request, error, errorInfo, null);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, null, row, segment);
finishedTask(request, null, segment);
} else {
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
@@ -4572,7 +4182,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
abortTask(fallbackTask, request, error),
);
boundary.fallbackAbortableTasks.clear();
return finishedTask(request, boundary, row, segment);
return finishedTask(request, boundary, segment);
}
}
boundary.status = CLIENT_RENDERED;
@@ -4589,7 +4199,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
logPostpone(request, postponeInstance.message, errorInfo, null);
if (request.trackedPostpones !== null && segment !== null) {
trackPostpone(request, request.trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, row, segment);
finishedTask(request, task.blockedBoundary, segment);
// If this boundary was still pending then we haven't already cancelled its fallbacks.
// We'll need to abort the fallbacks, which will also error that parent boundary.
@@ -4745,14 +4355,8 @@ function finishedSegment(
function finishedTask(
request: Request,
boundary: Root | SuspenseBoundary,
row: null | SuspenseListRow,
segment: null | Segment,
) {
if (row !== null) {
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
request.allPendingTasks--;
if (boundary === null) {
if (segment !== null && segment.parentFlushed) {
@@ -4801,13 +4405,6 @@ function finishedTask(
if (!isEligibleForOutlining(request, boundary)) {
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
boundary.fallbackAbortableTasks.clear();
const boundaryRow = boundary.row;
if (boundaryRow !== null) {
// If we aren't eligible for outlining, we don't have to wait until we flush it.
if (--boundaryRow.pendingTasks === 0) {
finishSuspenseListRow(request, boundaryRow);
}
}
}
if (
@@ -4906,7 +4503,7 @@ function retryRenderTask(
task.abortSet.delete(task);
segment.status = COMPLETED;
finishedSegment(request, task.blockedBoundary, segment);
finishedTask(request, task.blockedBoundary, task.row, segment);
finishedTask(request, task.blockedBoundary, segment);
} catch (thrownValue: mixed) {
resetHooksState();
@@ -4959,7 +4556,7 @@ function retryRenderTask(
}
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, task.row, segment);
finishedTask(request, task.blockedBoundary, segment);
return;
}
@@ -4993,7 +4590,7 @@ function retryRenderTask(
__DEV__ ? task.debugTask : null,
);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, task.row, segment);
finishedTask(request, task.blockedBoundary, segment);
return;
}
}
@@ -5005,7 +4602,6 @@ function retryRenderTask(
erroredTask(
request,
task.blockedBoundary,
task.row,
x,
errorInfo,
__DEV__ ? task.debugTask : null,
@@ -5053,7 +4649,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
task.replay.pendingTasks--;
task.abortSet.delete(task);
finishedTask(request, task.blockedBoundary, task.row, null);
finishedTask(request, task.blockedBoundary, null);
} catch (thrownValue) {
resetHooksState();
@@ -5365,16 +4961,6 @@ function flushSegment(
// Emit a client rendered suspense boundary wrapper.
// We never queue the inner boundary so we'll never emit its content or partial segments.
const row = boundary.row;
if (row !== null) {
// Since this boundary end up client rendered, we can unblock future suspense list rows.
// This means that they may appear out of order if the future rows succeed but this is
// a client rendered row.
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
if (__DEV__) {
writeStartClientRenderedSuspenseBoundary(
destination,
@@ -5463,16 +5049,6 @@ function flushSegment(
if (hoistableState) {
hoistHoistables(hoistableState, boundary.contentState);
}
const row = boundary.row;
if (row !== null && isEligibleForOutlining(request, boundary)) {
// Once we have written the boundary, we can unblock the row and let future
// rows be written. This may schedule new completed boundaries.
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
// We can inline this boundary's content as a complete boundary.
writeStartCompletedSuspenseBoundary(destination, request.renderState);
@@ -5551,15 +5127,6 @@ function flushCompletedBoundary(
}
completedSegments.length = 0;
const row = boundary.row;
if (row !== null && isEligibleForOutlining(request, boundary)) {
// Once we have written the boundary, we can unblock the row and let future
// rows be written. This may schedule new completed boundaries.
if (--row.pendingTasks === 0) {
finishSuspenseListRow(request, row);
}
}
writeHoistablesForBoundary(
destination,
boundary.contentState,
@@ -5752,7 +5319,6 @@ function flushCompletedQueues(
// Next we check the completed boundaries again. This may have had
// boundaries added to it in case they were too larged to be inlined.
// SuspenseListRows might have been unblocked as well.
// New ones might be added in this loop.
const largeBoundaries = request.completedBoundaries;
for (i = 0; i < largeBoundaries.length; i++) {

View File

@@ -16,5 +16,6 @@ declare const __IS_FIREFOX__: boolean;
declare const __IS_CHROME__: boolean;
declare const __IS_EDGE__: boolean;
declare const __IS_NATIVE__: boolean;
declare const __IS_INTERNAL_MCP_BUILD__: boolean;
declare const chrome: any;

View File

@@ -15,6 +15,7 @@ global.__IS_FIREFOX__ = false;
global.__IS_CHROME__ = false;
global.__IS_EDGE__ = false;
global.__IS_NATIVE__ = false;
global.__IS_INTERNAL_MCP_BUILD__ = false;
const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;