Compare commits

..

21 Commits

Author SHA1 Message Date
Jorge Cabiedes Acosta
6ee7b65c71 [compiler] Change ValidateNoDerivedComputationsInEffect logic to track prop and local state derived values variables and add extra tests
Summary:
Biggest change of the stack, we track how values prop and local state values are derived throughout the entire component.

This PR also adds a couple tests we will work towards fixing

Test Plan:
Added:
ref-conditional-in-effect-no-error
effect-contains-prop-function-call-no-error
derived-state-from-ref-and-state-no-error
2025-09-23 15:08:58 -07:00
Jorge Cabiedes Acosta
d966edf822 [compiler] ValidateNoDerivedComputationsInEffects test cases
Summary:
This creates the test cases we expect this first iteration of calculate in render to catch

Test Plan:
Test cases
2025-09-23 14:53:07 -07:00
Sebastian "Sebbie" Silbermann
720bb13069 [compiler] Export PluginOptions as a type that can be used in input positions (#34550) 2025-09-22 18:28:19 +02:00
Eugene Choi
1eca9a2747 [playground] Add compiler playground tests (#34528)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

Added more tests for the compiler playground with the addition of the
new config editor and "Show Internals" button. Added testing to check
for incomplete store params in the URL, toggle functionality, and
correct errors showing for syntax/validation errors in the config
overrides.
2025-09-22 12:11:45 -04:00
Sebastian "Sebbie" Silbermann
cd85bb5616 Include Fizz runtime diff in CI (#34525) 2025-09-22 17:09:50 +02:00
Sebastian "Sebbie" Silbermann
07e4974bad [compiler] Don't leak global __DEV__ type (#34551) 2025-09-22 16:51:57 +02:00
Sebastian Markbåge
d91d28c8ba Use the JSX of the ViewTransition as the Stack Trace of "Animating" Traces (#34539)
Stacked on #34538.

Track the Task of the first ViewTransition that we detected as
animating. Use this as the Task as "Starting Animation", "Animating"
etc. That way you can see which ViewTransition spawned the Animation.
Although it's likely to be multiple.

<img width="757" height="393" alt="Screenshot 2025-09-19 at 10 19 18 PM"
src="https://github.com/user-attachments/assets/a6cdcb89-bd02-40ec-b3c3-11121c29e892"
/>
2025-09-20 11:11:27 -04:00
Sebastian Markbåge
b4fe1e6c7e Log the time until the Animation finishes as "Animating" (#34538)
Stacked on #34522.

<img width="1025" height="200" alt="Screenshot 2025-09-19 at 6 37 28 PM"
src="https://github.com/user-attachments/assets/f25900f6-6503-48b1-876d-bd6697a29c6f"
/>

We already cover the time between "Starting Animation" and "Remaining
Effects" as "Animating". However, if the effects are forced then we can
still be animating after that. This fills in that gap.

This also fills in the gap if another render starts before the animation
finishes on the same track. It'll mark the blank space between the
previous render finishing and the next render starting as "Animating".

This should correspond roughly to the native "Animations" track.
2025-09-20 11:10:42 -04:00
Sebastian Markbåge
b204edda3a Log Custom Reason for the Suspended Commit Track (#34522)
Stacked on #34511.

We currently log all Suspended Commit as "Suspended on Images or CSS"
but it can really be other reasons too now. Like waiting on the previous
View Transition. This allows the host config configure this reason.

Now when one animation starts before another one finishes we log that as
"Waiting for the previous Animation".

<img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM"
src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3"
/>
2025-09-20 11:01:52 -04:00
Hendrik Liebau
115e3ec15f [ci] Document that full git shas are required for manual prereleases (#34537)
Triggering the "(Runtime) Publish Prereleases Manual" workflow with a
short git sha doesn't work. It needs the full sha. We might be able to
make it work with the short sha as well, but for now we can at least
document the restriction.
2025-09-20 08:09:44 +02:00
Sebastian Markbåge
565eb7888e Unwrap a reference to a Lazy value (#34535)
If we are referencing a lazy value that isn't explicitly lazy ($L...)
it's because we added it around an element that was blocked to be able
to defer things inside.

However, once that is unblocked we can start unwrap it and just use the
inner element instead for any future reference. The race condition is
still there since it's a race condition whether we added the wrapper in
the first place.

This just makes it consistent with unwrapping of the rest of the path.
2025-09-19 18:23:18 -04:00
Hendrik Liebau
d415fd3ed7 [Flight] Handle Lazy in renderDebugModel (#34536)
If we don't handle Lazy types specifically in `renderDebugModel`, all of
their properties will be emitted using `renderDebugModel` as well. This
also includes its `_debugInfo` property, if the Lazy comes from the
Flight client. That array might contain objects that are deduped, and
resolving those references in the client can cause runtime errors, e.g.:

```
TypeError: Cannot read properties of undefined (reading '$$typeof')
```

This happened specifically when an "RSC stream" debug info entry, coming
from the Flight client through IO tracking, was emitted and its
`debugTask` property was deduped, which couldn't be resolved in the
client.

To avoid actually initializing a lazy causing a side-effect, we make
some assumptions about the structure of its payload, and only emit
resolved or rejected values, otherwise we emit a halted chunk.
2025-09-19 23:38:11 +02:00
Jack Pope
5e3cd53f20 Update MAINTAINERS (#34534) 2025-09-19 15:49:08 -04:00
Janka Uryga
01cad9eaca [Flight] Support Async Modules in Turbopack Server References (#34531)
Seems like this was missed in
https://github.com/facebook/react/pull/31313
2025-09-19 12:12:37 -07:00
Eugene Choi
6eda534718 [playground] bug fixes & UX improvements (#34499)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Made many small changes to the compiler playground to improve user
experience. Removed any "Loading" indicators that would flash in before
a component would finish loading in. Additionally, before users would
see the "Show Internals" button toggling from false to true if they had
set it at true previously. I was able to refactor the URL/local storage
loading so that the `Store` would be fully initialized before the
components would load in.

Attempted to integrate `<Activity>` into showing/hiding these different
editors, but the current state of [monaco
editors](https://github.com/suren-atoyan/monaco-react) does not allow
for this. I created an issue for them to address:
https://github.com/suren-atoyan/monaco-react/issues/753

Added a debounce to the config editor so every key type wouldn't cause
the output panel to respond instantly. Users can type for 500 ms before
an error is thrown at them.

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

Here is what loading the page would look like before (not sure why its
so blurry):


https://github.com/user-attachments/assets/58f4281a-cc02-4141-b9b5-f70d6ace12a2


Here is how it looks now:


https://github.com/user-attachments/assets/40535165-fc7c-44fb-9282-9c7fa76e7d53

Here is the debouncing:


https://github.com/user-attachments/assets/e4ab29e4-1afd-4249-beca-671fb6542f5e



<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2025-09-18 15:44:25 -04:00
Hendrik Liebau
c03a51d836 Move getDebugInfo test util function to internal-test-utils (#34523)
In an upstack PR, I need `getDebugInfo` in another test file, so I'm
moving it to `internal-test-utils` so it can be shared.
2025-09-18 21:32:36 +02:00
Sebastian Markbåge
ad578aa01f Log Suspended startViewTransition Phase (#34511)
Stacked on #34510.

The "Commit" phase for a View Transition starts before the snapshot
phase (before mutation) and then stretches into the async gap of
`startViewTransition`, encompasses the mutation phase inside of its
update callback and finally the layout phase.

However, between the mutation phase and the layout phase we may suspend
the start of the view transition on fonts and/or images. In that case we
now split the Commit phase into first one before we suspend and then we
log "Waiting for Images and/or Fonts" and then another Commit phase
around the layout effects.

<img width="897" height="119" alt="Screenshot 2025-09-16 at 11 37 26 PM"
src="https://github.com/user-attachments/assets/0fe21388-bb48-4456-a594-62227d12d9b7"
/>
2025-09-18 15:25:41 -04:00
Sebastian "Sebbie" Silbermann
03a96c75db [DevTools] Record Suspense node for roots in legacy renderers (#34516) 2025-09-18 18:50:23 +02:00
Sebastian "Sebbie" Silbermann
755cebad6b [DevTools] Elevate Suspense rects to visualize hierarchy (#34455) 2025-09-18 18:37:00 +02:00
zeki
581321160f [Compiler Bug] Complier mark ts instantiation expression as reorderable in build hir (#34488)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
--> The React Compiler rejected a default parameter that contains a
TSInstantiationExpression with the todo message that the expression
cannot be safely reordered. This change teaches the reorder check in
BuildHIR.ts to treat TSInstantiationExpression as reorderable. This is
safe because TypeScript instantiation only affects types and is erased
at runtime, so it has no side effects and does not change semantics.

## How did you test this change?

```
Set-Content testfilter.txt 'ts-instantiation-default-param'

yarn test --filter --update

yarn test --filter
```


<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
--> I added a fixture: 
>
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-instantiation-default-param.js
2025-09-18 09:34:47 -07:00
Joseph Savona
1bcdd224b1 [compiler] Don't show hint about ref-like naming if we infer another type (#34521)
Some components accept a union of a ref callback function or ref object.
In this case we may infer the type as a function due to the presence of
invoking the ref callback function. In that case, we currently report a
"Hint: name `fooRef` as "ref" or with a "-Ref" suffix..." even though
the variable is already named appropriately — the problem is that we
inferred a non-ref type. So here we check the type and don't report this
hint if we inferred another type.
2025-09-18 09:26:10 -07:00
95 changed files with 2740 additions and 1025 deletions

View File

@@ -194,7 +194,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
@@ -567,7 +567,7 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies

View File

@@ -1,5 +1,6 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

@@ -0,0 +1,5 @@
import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
  //compilationMode: "all"
} satisfies Partial<PluginOptions>);

View File

@@ -0,0 +1,14 @@
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x || true) {
t1 = <Button>{x}</Button>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

View File

@@ -5,8 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test} from '@playwright/test';
import {expect, test, type Page} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -20,6 +21,15 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
expandButton.click();
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -121,10 +131,9 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -136,7 +145,7 @@ test('editor should compile from hash successfully', async ({page}) => {
path: 'test-results/01-compiles-from-hash.png',
});
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -145,10 +154,9 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -157,33 +165,171 @@ test('reset button works', async ({page}) => {
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/02-reset-button-works.png',
});
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: "123"
} satisfies Partial<PluginOptions>);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies Partial<PluginOptions>);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/03-0${idx}-${t.name}.png`,
path: `test-results/08-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -1,56 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Lt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<StoreProvider>
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
<Header />
<Editor />
</SnackbarProvider>
</StoreProvider>
</div>
);
};
export default Home;

View File

@@ -17,15 +17,6 @@ export default function AccordionWindow(props: {
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
if (props.tabs.size === 0) {
return (
<div
className="flex items-center justify-center"
style={{width: 'calc(100vw - 650px)'}}>
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-row h-full">
{Array.from(props.tabs.keys()).map(name => {

View File

@@ -9,7 +9,7 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {PluginOptions} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {useState} from 'react';
import React, {useState, useRef} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
@@ -28,10 +28,25 @@ export default function ConfigEditor({
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
return isExpanded ? (
<ExpandedEditor onToggle={setIsExpanded} appliedOptions={appliedOptions} />
) : (
<CollapsedEditor onToggle={setIsExpanded} />
return (
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
<ExpandedEditor
onToggle={setIsExpanded}
appliedOptions={appliedOptions}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
<CollapsedEditor onToggle={setIsExpanded} />
</div>
</>
);
}
@@ -44,16 +59,25 @@ function ExpandedEditor({
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const handleChange: (value: string | undefined) => void = value => {
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
@@ -77,12 +101,6 @@ function ExpandedEditor({
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
const uri = monaco.Uri.parse(`file:///config.ts`);
const model = monaco.editor.getModel(uri);
if (model) {
model.updateOptions({tabSize: 2});
}
};
const formattedAppliedOptions = appliedOptions
@@ -126,6 +144,8 @@ function ExpandedEditor({
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={{
...monacoOptions,
lineNumbers: 'off',
@@ -139,7 +159,6 @@ function ExpandedEditor({
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
@@ -151,6 +170,8 @@ function ExpandedEditor({
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedOptions}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoOptions,
lineNumbers: 'off',

View File

@@ -24,19 +24,8 @@ import BabelPluginReactCompiler, {
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
import {useDeferredValue, useMemo} from 'react';
import {useMountEffect} from '../../hooks';
import {defaultStore} from '../../lib/defaultStore';
import {
createMessage,
initStoreFromUrlOrLocalStorage,
MessageLevel,
MessageSource,
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import {useStore} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {
@@ -174,7 +163,6 @@ function parseOptions(
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
// TODO: initialize store with URL params, not empty store
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
@@ -327,8 +315,6 @@ function compile(
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language, appliedOptions] = useMemo(
() => compile(deferredStore.source, 'compiler', deferredStore.config),
[deferredStore.source, deferredStore.config],
@@ -338,32 +324,6 @@ export default function Editor(): JSX.Element {
[deferredStore.source, deferredStore.config],
);
useMountEffect(() => {
// Initialize store
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,
MessageSource.Playground,
),
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {
store: mountStore,
},
});
});
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {

View File

@@ -13,7 +13,6 @@ import {
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
@@ -46,11 +45,6 @@ export default function Input({errors, language}: Props): JSX.Element {
details: errors,
source: store.source,
});
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors, store.source]);
useEffect(() => {
@@ -151,39 +145,26 @@ export default function Input({errors, language}: Props): JSX.Element {
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>
);
const tabs = new Map([['Input', editorContent]]);
const [activeTab, setActiveTab] = useState('Input');
const tabbedContent = (
<div className="flex flex-col h-full">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
);
return (
<div className="relative flex flex-col flex-none border-r border-gray-200">
{store.showInternals ? (
<Resizable
minWidth={550}
enable={{right: true}}
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
className="!h-[calc(100vh_-_3.5rem)]">
{tabbedContent}
</Resizable>
) : (
<div className="!h-[calc(100vh_-_3.5rem)]">{tabbedContent}</div>
)}
<div className="!h-[calc(100vh_-_3.5rem)]">
<div className="flex flex-col h-full">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</div>
);
}

View File

@@ -324,6 +324,7 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
readOnly: true,
@@ -338,6 +339,8 @@ function TextTabContent({
<MonacoEditor
language={language ?? 'javascript'}
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -29,4 +29,6 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'same',
tabSize: 2,
};

View File

@@ -58,7 +58,7 @@ export default function Header(): JSX.Element {
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="relative inline-block w-[34px] h-5">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}

View File

@@ -6,10 +6,14 @@
*/
import type {Dispatch, ReactNode} from 'react';
import {useEffect, useReducer} from 'react';
import {useState, useEffect, useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore} from '../lib/defaultStore';
import {saveStore, type Store} from '../lib/stores';
import {emptyStore, defaultStore} from '../lib/defaultStore';
import {
saveStore,
initStoreFromUrlOrLocalStorage,
type Store,
} from '../lib/stores';
const StoreContext = createContext<Store>();
@@ -30,6 +34,20 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
*/
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
const [isPageReady, setIsPageReady] = useState<boolean>(false);
useEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
console.error('Failed to initialize store from URL or local storage', e);
mountStore = defaultStore;
}
dispatch({type: 'setStore', payload: {store: mountStore}});
setIsPageReady(true);
}, []);
useEffect(() => {
if (store !== emptyStore) {
saveStore(store);
@@ -39,7 +57,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
return (
<StoreContext.Provider value={store}>
<StoreDispatchContext.Provider value={dispatch}>
{children}
{isPageReady ? children : null}
</StoreDispatchContext.Provider>
</StoreContext.Provider>
);

View File

@@ -16,13 +16,6 @@ export default function TabbedWindow({
activeTab: string;
onTabChange: (tab: string) => void;
}): React.ReactElement {
if (tabs.size === 0) {
return (
<div className="flex items-center justify-center flex-1 max-w-full">
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">

View File

@@ -18,7 +18,7 @@ import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
//compilationMode: "all"
} satisfies Partial<PluginOptions>);`;
} satisfies PluginOptions);`;
export const defaultStore: Store = {
source: index,

View File

@@ -71,7 +71,7 @@ export function initStoreFromUrlOrLocalStorage(): Store {
// Make sure all properties are populated
return {
source: raw.source,
config: 'config' in raw ? raw.config : defaultConfig,
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
}

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -34,7 +34,7 @@
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.2",
"next": "15.6.0-canary.7",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.12",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",

View File

@@ -6,6 +6,9 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -16,7 +19,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{

View File

@@ -715,10 +715,10 @@
dependencies:
"@monaco-editor/loader" "^1.4.0"
"@next/env@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.2.tgz#0c6b959313cd6e71afb69bf0deb417237f1d2f8a"
integrity sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==
"@next/env@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.7.tgz#cdbf2967a9437ef09eef755e203f315acc4d8d8f"
integrity sha512-LNZ7Yd3Cl9rKvjYdeJmszf2HmSDP76SQmfafKep2Ux16ZXKoN5OjwVHFTltKNdsB3vt2t+XJzLP2rhw5lBoFBA==
"@next/eslint-plugin-next@15.5.2":
version "15.5.2"
@@ -727,45 +727,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz#f69713326fc08f2eff3726fe19165cdb429d67c7"
integrity sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==
"@next/swc-darwin-arm64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.7.tgz#628cd34ce9120000f1cb5b08963426431174fc57"
integrity sha512-POsBrxhrR3qvqXV+JZ6ZoBc8gJf8rhYe+OedceI1piPVqtJYOJa3EB4eaqcc+kMsllKRrH/goNlhLwtyhE+0Qg==
"@next/swc-darwin-x64@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz#560a9da4126bae75cbbd6899646ad7a2e4fdcc9b"
integrity sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==
"@next/swc-darwin-x64@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.7.tgz#37d4ebab14da74a2f8028daf6d76aab410153e06"
integrity sha512-lmk9ysBuSiPlAJZTCo/3O4mXNFosg6EDIf4GrmynIwCG2as6/KxzyD1WqFp56Exp8eFDjP7SFapD10sV43vCsA==
"@next/swc-linux-arm64-gnu@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz#80b2be276e775e5a9286369ae54e536b0cdf8c3a"
integrity sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==
"@next/swc-linux-arm64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.7.tgz#ce700cc0e0d24763136838223105a524b36694fa"
integrity sha512-why8k6d0SBm3AKoOD5S7ir3g+BF34l9oFKIoZrLaZaKBvNGpFcjc7Ovc2TunNMeaMJzv9k1dHYSap0EI5oSuzg==
"@next/swc-linux-arm64-musl@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz#68cf676301755fd99aca11a7ebdb5eae88d7c2e4"
integrity sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==
"@next/swc-linux-arm64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.7.tgz#c791b8e15bf2c338b4cc0387fe7afb3ef83ecfcf"
integrity sha512-HzvTRsKvYj32Va4YuJN3n3xOxvk+6QwB63d/EsgmdkeA/vrqciUAmJDYpuzZEvRc3Yp2nyPq8KZxtHAr6ISZ2Q==
"@next/swc-linux-x64-gnu@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz#209d9a79d0f2333544f863b0daca3f7e29f2eaff"
integrity sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==
"@next/swc-linux-x64-gnu@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.7.tgz#c01c3a3d8e71660c49298dd053d078379b6b5919"
integrity sha512-6yRFrg2qWXOqa+1BI53J9EmHWFzKg9U2r+5R7n7BFUp8PH5SC92WBsmYTnh/RkvAYvdupiVzMervwwswCs6kFg==
"@next/swc-linux-x64-musl@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz#d4ad1cfb5e99e51db669fe2145710c1abeadbd7f"
integrity sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==
"@next/swc-linux-x64-musl@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.7.tgz#3f4b39faef4a5f88b13e4c726b008ddc9717f819"
integrity sha512-O/JjvOvNK/Wao/OIQaA6evDkxkmFFQgJ1/hI1dVk6/PAeKmW2/Q+6Dodh97eAkOwedS1ZdQl2mojf87TzLvzdQ==
"@next/swc-win32-arm64-msvc@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz#070e10e370a5447a198c2db100389646aca2c496"
integrity sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==
"@next/swc-win32-arm64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.7.tgz#9bc5da0907b7ce67eedda02a6d56a09d9a539ccf"
integrity sha512-p9DvrDgnePofZCtiWVY7qZtwXxiOGJlAyy2LoGPYSGOUDhjbTG8j6XMUFXpV9UwpH+l7st522psO1BVzbpT8IQ==
"@next/swc-win32-x64-msvc@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz#9237d40b82eaf2efc88baeba15b784d4126caf4a"
integrity sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==
"@next/swc-win32-x64-msvc@15.6.0-canary.7":
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.7.tgz#5b271c591ccbe67d5fa966dd22db86c547414fd1"
integrity sha512-f1ywT3xWu4StWKA1mZRyGfelu/h+W0OEEyBxQNXzXyYa0VGZb9LyCNb5cYoNKBm0Bw18Hp1PVe0bHuusemGCcw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -866,6 +866,13 @@
dependencies:
csstype "^3.0.2"
"@types/react@19.1.13":
version "19.1.13"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
dependencies:
csstype "^3.0.2"
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz#9c8218ed62f9a322df10ded7c34990f014df44f2"
@@ -3199,25 +3206,25 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@15.5.2:
version "15.5.2"
resolved "https://registry.yarnpkg.com/next/-/next-15.5.2.tgz#5e50102443fb0328a9dfcac2d82465c7bac93693"
integrity sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==
next@15.6.0-canary.7:
version "15.6.0-canary.7"
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.7.tgz#bfc2ac3c9a78e23d550c303d18247a263e6b5bc1"
integrity sha512-4ukX2mxat9wWT6E0Gw/3TOR9ULV1q399E42F86cwsPSFgTWa04ABhcTqO0r9J/QR1YWPR8WEgh9qUzmWA/1yEw==
dependencies:
"@next/env" "15.5.2"
"@next/env" "15.6.0-canary.7"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.5.2"
"@next/swc-darwin-x64" "15.5.2"
"@next/swc-linux-arm64-gnu" "15.5.2"
"@next/swc-linux-arm64-musl" "15.5.2"
"@next/swc-linux-x64-gnu" "15.5.2"
"@next/swc-linux-x64-musl" "15.5.2"
"@next/swc-win32-arm64-msvc" "15.5.2"
"@next/swc-win32-x64-msvc" "15.5.2"
"@next/swc-darwin-arm64" "15.6.0-canary.7"
"@next/swc-darwin-x64" "15.6.0-canary.7"
"@next/swc-linux-arm64-gnu" "15.6.0-canary.7"
"@next/swc-linux-arm64-musl" "15.6.0-canary.7"
"@next/swc-linux-x64-gnu" "15.6.0-canary.7"
"@next/swc-linux-x64-musl" "15.6.0-canary.7"
"@next/swc-win32-arm64-msvc" "15.6.0-canary.7"
"@next/swc-win32-x64-msvc" "15.6.0-canary.7"
sharp "^0.34.3"
node-releases@^2.0.18:

View File

@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: Partial<PluginOptions> | null,
options: PluginOptions | null,
includeAst: boolean = false,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {

View File

@@ -18,7 +18,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, PluginOptions} from './Options';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
@@ -66,7 +66,7 @@ export class ProgramContext {
* Program and environment context
*/
scope: BabelScope;
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;

View File

@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
logger: Logger | null;
@@ -166,7 +166,11 @@ export type PluginOptions = {
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
};
}>;
export type ParsedPluginOptions = Required<
Omit<PluginOptions, 'environment'>
> & {environment: EnvironmentConfig};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
@@ -282,7 +286,7 @@ export type Logger = {
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: PluginOptions = {
export const defaultOptions: ParsedPluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
@@ -299,9 +303,9 @@ export const defaultOptions: PluginOptions = {
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
};
export function parsePluginOptions(obj: unknown): PluginOptions {
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}

View File

@@ -23,7 +23,11 @@ import {
ProgramContext,
validateRestrictedImports,
} from './Imports';
import {CompilerReactTarget, PluginOptions} from './Options';
import {
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
@@ -34,7 +38,7 @@ import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
@@ -45,7 +49,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
opts: PluginOptions,
opts: ParsedPluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
@@ -81,7 +85,7 @@ export function findDirectiveDisablingMemoization(
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: PluginOptions,
opts: ParsedPluginOptions,
): Result<
{
gating: ExternalFunction;

View File

@@ -7,7 +7,7 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {PluginOptions} from './Options';
import {ParsedPluginOptions} from './Options';
function hasModule(name: string): boolean {
if (typeof require === 'undefined') {
@@ -52,7 +52,9 @@ export function pipelineUsesReanimatedPlugin(
return hasModule('react-native-reanimated');
}
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
export function injectReanimatedFlag(
options: ParsedPluginOptions,
): ParsedPluginOptions {
return {
...options,
environment: {

View File

@@ -3081,6 +3081,12 @@ function isReorderableExpression(
return true;
}
}
case 'TSInstantiationExpression': {
const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
'expression',
) as NodePath<t.Expression>;
return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
}
case 'RegExpLiteral':
case 'StringLiteral':
case 'NumericLiteral':

View File

@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
});
}
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
const testComplexPluginOptionDefaults: PluginOptions = {
gating: {
source: 'ReactForgetFeatureFlag',
importSpecifierName: 'isForgetEnabled_Fixtures',

View File

@@ -5,21 +5,31 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
Place,
CallExpression,
Instruction,
isUseStateType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sourcesIds: Set<IdentifierId>;
};
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -45,102 +55,226 @@ import {
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
derivationCache.set(param.identifier.id, {
place: param,
sourcesIds: new Set([param.identifier.id]),
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivationCache.set(props.identifier.id, {
place: props,
sourcesIds: new Set([props.identifier.id]),
typeOfValue: 'fromProps',
});
}
}
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
for (const phi of block.phis) {
let typeOfValue: TypeOfValue = 'ignored';
let sourcesIds: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const operandMetadata = derivationCache.get(operand.identifier.id);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sourcesIds.add(operand.identifier.id);
}
if (typeOfValue !== 'ignored') {
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
}
}
for (const i of block.instructions) {
function recordInstructiorDerivations(instr: Instruction): void {
let typeOfValue: TypeOfValue = 'ignored';
const sources: Set<IdentifierId> = new Set();
const {lvalue, value} = instr;
if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
for (const [, block] of value.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
recordInstructiorDerivations(instr);
}
}
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
const effectFunction = functions.get(value.args[0].identifier.id);
if (effectFunction != null) {
validateEffect(
effectFunction.loweredFunc.func,
errors,
derivationCache,
);
}
} else if (isUseStateType(lvalue.identifier)) {
const stateValueSource = value.args[0];
if (stateValueSource.kind === 'Identifier') {
sources.add(stateValueSource.identifier.id);
}
typeOfValue = joinValue(typeOfValue, 'fromState');
}
}
for (const operand of eachInstructionOperand(instr)) {
const operandMetadata = derivationCache.get(operand.identifier.id);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
for (const id of operandMetadata.sourcesIds) {
sources.add(id);
}
}
if (typeOfValue === 'ignored') {
return;
}
for (const lvalue of eachInstructionLValue(instr)) {
addDerivationEntry(lvalue, sources, typeOfValue, derivationCache);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
addDerivationEntry(
operand,
sources,
typeOfValue,
derivationCache,
);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: value.loc,
message: 'this is checked as a place above',
loc: operand.loc,
message: 'Unexpected unknown effect',
},
],
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
recordInstructiorDerivations(i);
}
}
if (errors.hasAnyErrors()) {
throw errors;
}
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsAndState';
}
function addDerivationEntry(
derivedVar: Place,
sourcesIds: Set<IdentifierId>,
typeOfValue: TypeOfValue,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
let newValue: DerivationMetadata = {
place: derivedVar,
sourcesIds: new Set(),
typeOfValue: typeOfValue ?? 'ignored',
};
if (sourcesIds !== undefined) {
for (const id of sourcesIds) {
const sourcePlace = derivationCache.get(id)?.place;
if (sourcePlace === undefined) {
continue;
}
/*
* If the identifier of the source is a promoted identifier, then
* we should set the target as the source.
*/
if (
sourcePlace.identifier.name === null ||
sourcePlace.identifier.name?.kind === 'promoted'
) {
newValue.sourcesIds.add(derivedVar.identifier.id);
} else {
newValue.sourcesIds.add(sourcePlace.identifier.id);
}
}
}
derivationCache.set(derivedVar.identifier.id, newValue);
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
const effectDerivedSetStateCalls: Array<{
value: CallExpression;
sourceIds: Set<IdentifierId>;
}> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
@@ -148,90 +282,36 @@ function validateEffect(
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const argMetadata = derivationCache.get(
instr.value.args[0].identifier.id,
);
if (argMetadata !== undefined) {
effectDerivedSetStateCalls.push({
value: instr.value,
sourceIds: argMetadata.sourcesIds,
});
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
loc: derivedSetStateCall.value.callee.loc,
suggestions: null,
});
}

View File

@@ -0,0 +1,47 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-conditionally-in-effect.ts:9:6
7 | useEffect(() => {
8 | if (enabled) {
> 9 | setLocalValue(value);
| ^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | } else {
11 | setLocalValue('disabled');
12 | }
```

View File

@@ -0,0 +1,21 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-default-props.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setCurrInput(input + localConst);
| ^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [input, localConst]);
11 |
12 | return <div>{currInput}</div>;
```

View File

@@ -0,0 +1,18 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};

View File

@@ -0,0 +1,51 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName}) {
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John');
const middleName = 'D.';
useEffect(() => {
setFullName(firstName + ' ' + middleName + ' ' + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
9 |
10 | useEffect(() => {
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [firstName, middleName, lastName]);
13 |
14 | return (
```

View File

@@ -0,0 +1,25 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName}) {
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John');
const middleName = 'D.';
useEffect(() => {
setFullName(firstName + ' ' + middleName + ' ' + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};

View File

@@ -0,0 +1,47 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-setter-call-outside-effect-no-error.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setName(initialName);
| ^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [initialName]);
10 |
11 | return (
```

View File

@@ -0,0 +1,21 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};

View File

@@ -0,0 +1,46 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-setter-used-outside-effect-no-error.ts:11:4
9 | const [value, setValue] = useState(null);
10 | useEffect(() => {
> 11 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [propValue]);
13 |
14 | return <MockComponent onSet={setValue} />;
```

View File

@@ -0,0 +1,20 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-with-side-effect.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setLocalValue(value);
| ^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | document.title = `Value: ${value}`;
10 | }, [value]);
11 |
```

View File

@@ -0,0 +1,18 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState('');
const myRef = useRef(null);
useEffect(() => {
setLocal(myRef.current + test);
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 'testString'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-ref-and-state-no-error.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setLocal(myRef.current + test);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
11 | }, [test]);
12 |
13 | return <>{local}</>;
```

View File

@@ -0,0 +1,19 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState('');
const myRef = useRef(null);
useEffect(() => {
setLocal(myRef.current + test);
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 'testString'}],
};

View File

@@ -0,0 +1,48 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.effect-contains-local-function-call.ts:12:4
10 |
11 | useEffect(() => {
> 12 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
13 | localFunction();
14 | }, [propValue]);
15 |
```

View File

@@ -0,0 +1,22 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
onChange();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test', onChange: () => {}}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.effect-contains-prop-function-call-no-error.ts:7:4
5 | const [value, setValue] = useState(null);
6 | useEffect(() => {
> 7 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
8 | onChange();
9 | }, [propValue]);
10 |
```

View File

@@ -0,0 +1,17 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
onChange();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test', onChange: () => {}}],
};

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.effect-with-global-function-call-no-error.ts:7:4
5 | const [value, setValue] = useState(null);
6 | useEffect(() => {
> 7 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
8 | globalCall();
9 | }, [propValue]);
10 |
```

View File

@@ -0,0 +1,17 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -0,0 +1,46 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:11:4
9 | const [fullName, setFullName] = useState('');
10 | useEffect(() => {
> 11 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [firstName, lastName]);
13 |
14 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,20 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-state-from-computed-props.ts:9:4
7 | useEffect(() => {
8 | const computed = props.prefix + props.value + props.suffix;
> 9 | setDisplayValue(computed);
| ^^^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [props.prefix, props.value, props.suffix]);
11 |
12 | return <div>{displayValue}</div>;
```

View File

@@ -0,0 +1,18 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-state-from-destructured-props.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(props.firstName + ' ' + props.lastName);
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
11 | }, [props.firstName, props.lastName]);
12 |
13 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,19 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState(0);
const myRef = useRef(null);
useEffect(() => {
if (myRef.current) {
setLocal(test);
} else {
setLocal(test + test);
}
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 4}],
};
```
## Error
```
Found 2 errors:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.ref-conditional-in-effect-no-error.ts:11:6
9 | useEffect(() => {
10 | if (myRef.current) {
> 11 | setLocal(test);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | } else {
13 | setLocal(test + test);
14 | }
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.ref-conditional-in-effect-no-error.ts:13:6
11 | setLocal(test);
12 | } else {
> 13 | setLocal(test + test);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
14 | }
15 | }, [test]);
16 |
```

View File

@@ -0,0 +1,23 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {
const [local, setLocal] = useState(0);
const myRef = useRef(null);
useEffect(() => {
if (myRef.current) {
setLocal(test);
} else {
setLocal(test + test);
}
}, [test]);
return <>{local}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{test: 4}],
};

View File

@@ -3,6 +3,8 @@
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -10,7 +12,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
@@ -26,14 +28,14 @@ Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
error.invalid-derived-computation-in-effect.ts:11:4
9 | const [fullName, setFullName] = useState('');
10 | useEffect(() => {
> 11 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
12 | }, [firstName, lastName]);
13 |
14 | return <div>{fullName}</div>;
```

View File

@@ -1,4 +1,6 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -6,7 +8,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;

View File

@@ -0,0 +1,62 @@
## Input
```javascript
function id<T>(x: T): T {
return x;
}
export function Component<T = string>({fn = id<T>}: {fn?: (x: T) => T}) {
const value = fn('hi' as T);
return <div>{String(value)}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function id(x) {
return x;
}
export function Component(t0) {
const $ = _c(4);
const { fn: t1 } = t0;
const fn = t1 === undefined ? id : t1;
let t2;
if ($[0] !== fn) {
t2 = fn("hi" as T);
$[0] = fn;
$[1] = t2;
} else {
t2 = $[1];
}
const value = t2;
const t3 = String(value);
let t4;
if ($[2] !== t3) {
t4 = <div>{t3}</div>;
$[2] = t3;
$[3] = t4;
} else {
t4 = $[3];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div>hi</div>

View File

@@ -0,0 +1,13 @@
function id<T>(x: T): T {
return x;
}
export function Component<T = string>({fn = id<T>}: {fn?: (x: T) => T}) {
const value = fn('hi' as T);
return <div>{String(value)}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -51,6 +51,7 @@ export {
} from './ReactiveScopes';
export {parseConfigPragmaForTests} from './Utils/TestUtils';
declare global {
// @internal
let __DEV__: boolean | null | undefined;
}

View File

@@ -10,6 +10,7 @@
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"stripInternal": true,
"useUnknownInCatchVariables": true,
"target": "ES2015",
// ideally turn off only during dev, or on a per-file basis

View File

@@ -24,7 +24,7 @@ import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal

View File

@@ -46,7 +46,7 @@ const logger = {
},
};
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
compilationMode: 'infer',
panicThreshold: 'critical_errors',
@@ -72,7 +72,7 @@ function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: Partial<PluginOptions> | null,
options: PluginOptions | null,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {
sourceFilename: file,

View File

@@ -27,7 +27,7 @@ export type PrintedCompilerPipelineValue =
type CompileOptions = {
text: string;
file: string;
options: Partial<PluginOptions> | null;
options: PluginOptions | null;
};
export async function compile({
text,

View File

@@ -145,7 +145,7 @@ server.tool(
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
const compilerOptions: PluginOptions = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,

View File

@@ -25,12 +25,12 @@ import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,
environment: validateEnvironmentConfig({
environment: {
validateRefAccessDuringRender: true,
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
@@ -43,7 +43,7 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
validateNoCapitalizedCalls: [],
validateHooksUsage: true,
validateNoDerivedComputationsInEffects: true,
}),
},
};
export type UnusedOptOutDirective = {
@@ -113,7 +113,7 @@ function runReactCompilerImpl({
userOpts,
}: RunParams): RunCacheEntry {
// Compat with older versions of eslint
const options: PluginOptions = parsePluginOptions({
const options = parsePluginOptions({
...COMPILER_OPTIONS,
...userOpts,
environment: {

View File

@@ -16,6 +16,7 @@ import {
clearErrors,
createLogAssertion,
} from './consoleMock';
export {getDebugInfo} from './debugInfo';
export {act, serverAct} from './internalAct';
const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');

View File

@@ -0,0 +1,131 @@
'use strict';
const path = require('path');
const repoRoot = path.resolve(__dirname, '../../');
type DebugInfoConfig = {
ignoreProps?: boolean,
ignoreRscStreamInfo?: boolean,
useFixedTime?: boolean,
useV8Stack?: boolean,
};
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
function normalizeStack(stack) {
if (!stack) {
return stack;
}
const copy = [];
for (let i = 0; i < stack.length; i++) {
const [name, file, line, col, enclosingLine, enclosingCol] = stack[i];
copy.push([
name,
file.replace(repoRoot, ''),
line,
col,
enclosingLine,
enclosingCol,
]);
}
return copy;
}
function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
const {debugTask, debugStack, debugLocation, ...copy} = ioInfo;
if (ioInfo.stack) {
copy.stack = config.useV8Stack
? formatV8Stack(ioInfo.stack)
: normalizeStack(ioInfo.stack);
}
if (ioInfo.owner) {
copy.owner = normalizeDebugInfo(config, ioInfo.owner);
}
if (typeof ioInfo.start === 'number' && config.useFixedTime) {
copy.start = 0;
}
if (typeof ioInfo.end === 'number' && config.useFixedTime) {
copy.end = 0;
}
const promise = ioInfo.value;
if (promise) {
promise.then(); // init
if (promise.status === 'fulfilled') {
if (ioInfo.name === 'RSC stream') {
copy.byteSize = 0;
copy.value = {
value: 'stream',
};
} else {
copy.value = {
value: promise.value,
};
}
} else if (promise.status === 'rejected') {
copy.value = {
reason: promise.reason,
};
} else {
copy.value = {
status: promise.status,
};
}
}
return copy;
}
function normalizeDebugInfo(config: DebugInfoConfig, original) {
const {debugTask, debugStack, debugLocation, ...debugInfo} = original;
if (original.owner) {
debugInfo.owner = normalizeDebugInfo(config, original.owner);
}
if (original.awaited) {
debugInfo.awaited = normalizeIOInfo(config, original.awaited);
}
if (debugInfo.props && config.ignoreProps) {
debugInfo.props = {};
}
if (Array.isArray(debugInfo.stack)) {
debugInfo.stack = config.useV8Stack
? formatV8Stack(debugInfo.stack)
: normalizeStack(debugInfo.stack);
return debugInfo;
} else if (typeof debugInfo.time === 'number' && config.useFixedTime) {
return {...debugInfo, time: 0};
} else {
return debugInfo;
}
}
export function getDebugInfo(config: DebugInfoConfig, obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
if (
debugInfo[i].awaited &&
debugInfo[i].awaited.name === 'RSC stream' &&
config.ignoreRscStreamInfo
) {
// Ignore RSC stream I/O info.
} else {
copy.push(normalizeDebugInfo(config, debugInfo[i]));
}
}
return copy;
}
return debugInfo;
}

View File

@@ -621,6 +621,10 @@ export function waitForCommitToBeReady(timeoutOffset) {
return null;
}
export function getSuspendedCommitReason(state, rootContainer) {
return null;
}
export const NotPendingTransition = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -1337,7 +1337,11 @@ function fulfillReference(
const {response, handler, parentObject, key, map, path} = reference;
for (let i = 1; i < path.length; i++) {
while (value.$$typeof === REACT_LAZY_TYPE) {
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// We never expect to see a Lazy node on this path because we encode those as
// separate models. This must mean that we have inserted an extra lazy node
// e.g. to replace a blocked element. We must instead look for it inside.
@@ -1408,6 +1412,39 @@ function fulfillReference(
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
if (referencedChunk === handler.chunk) {
// This is a reference to the thing we're currently blocking. We can peak
// inside of it to get the value.
value = handler.value;
continue;
} else {
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
}
}
break;
}
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
@@ -1855,7 +1892,11 @@ function getOutlinedModel<T>(
case INITIALIZED:
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
while (value.$$typeof === REACT_LAZY_TYPE) {
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
const referencedChunk: SomeChunk<any> = value._payload;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
@@ -1924,6 +1965,32 @@ function getOutlinedModel<T>(
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
}
break;
}
const chunkValue = map(response, value, parentObject, key);
if (
parentObject[0] === REACT_ELEMENT_TYPE &&

View File

@@ -33,20 +33,6 @@ function normalizeCodeLocInfo(str) {
);
}
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
const repoRoot = path.resolve(__dirname, '../../../../');
function normalizeReactCodeLocInfo(str) {
const repoRootForRegexp = repoRoot.replace(/\//g, '\\/');
@@ -67,35 +53,6 @@ function getErrorForJestMatcher(error) {
};
}
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
}
return copy;
} else {
return debugInfo;
}
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
// Ignore RSC stream I/O info.
} else {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
}
return copy;
}
return debugInfo;
}
const finalizationRegistries = [];
function FinalizationRegistryMock(callback) {
this._heldValues = [];
@@ -132,6 +89,7 @@ let NoErrorExpected;
let Scheduler;
let assertLog;
let assertConsoleErrorDev;
let getDebugInfo;
describe('ReactFlight', () => {
beforeEach(() => {
@@ -169,6 +127,11 @@ describe('ReactFlight', () => {
assertLog = InternalTestUtils.assertLog;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
getDebugInfo = InternalTestUtils.getDebugInfo.bind(null, {
useV8Stack: true,
ignoreRscStreamInfo: true,
});
ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {

View File

@@ -18,50 +18,12 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.FormData = require('undici').FormData;
}
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
}
return copy;
} else {
return debugInfo;
}
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
return copy;
}
return debugInfo;
}
let act;
let React;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
let getDebugInfo;
describe('ReactFlight', () => {
beforeEach(() => {
@@ -91,6 +53,11 @@ describe('ReactFlight', () => {
ReactNoop = require('react-noop-renderer');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('internal-test-utils').act;
getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
useV8Stack: true,
ignoreRscStreamInfo: true,
});
});
afterEach(() => {

View File

@@ -34,6 +34,8 @@ import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
UNKNOWN_SUSPENDERS_NONE,
} from '../../constants';
import {decorateMany, forceUpdate, restoreMany} from './utils';
@@ -411,6 +413,13 @@ export function attach(
pushOperation(0); // StrictMode supported?
pushOperation(hasOwnerMetadata ? 1 : 0);
pushOperation(supportsTogglingSuspense ? 1 : 0);
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(parentID);
pushOperation(getStringID(null)); // name
// TODO: Measure rect of root
pushOperation(-1);
} else {
const type = getElementType(internalInstance);
const {displayName, key} = getData(internalInstance);
@@ -449,7 +458,12 @@ export function attach(
}
function recordUnmount(internalInstance: InternalInstance, id: number) {
pendingUnmountedIDs.push(id);
const isRoot = parentIDStack.length === 0;
if (isRoot) {
pendingUnmountedRootID = id;
} else {
pendingUnmountedIDs.push(id);
}
idToInternalInstanceMap.delete(id);
}
@@ -519,6 +533,8 @@ export function attach(
// All unmounts are batched in a single message.
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
// [SUSPENSE_TREE_OPERATION_REMOVE, 1, pendingUnmountedRootID]
(pendingUnmountedRootID === null ? 0 : 3) +
// Mount operations
pendingOperations.length,
);
@@ -555,6 +571,10 @@ export function attach(
if (pendingUnmountedRootID !== null) {
operations[i] = pendingUnmountedRootID;
i++;
operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;
operations[i++] = 1;
operations[i++] = pendingUnmountedRootID;
}
}

View File

@@ -163,6 +163,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-scroll-track': '#fafafa',
'--color-tooltip-background': 'rgba(0, 0, 0, 0.9)',
'--color-tooltip-text': '#ffffff',
'--elevation-4':
'0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)',
},
dark: {
'--color-attribute-name': '#9d87d2',
@@ -315,6 +318,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-scroll-track': '#313640',
'--color-tooltip-background': 'rgba(255, 255, 255, 0.95)',
'--color-tooltip-text': '#000000',
'--elevation-4':
'0 2px 8px 0 rgba(0,0,0,0.32),0 4px 12px 0 rgba(0,0,0,0.24),0 1px 10px 0 rgba(0,0,0,0.18)',
},
compact: {
'--font-size-monospace-small': '9px',

View File

@@ -895,11 +895,11 @@ export default class Store extends EventEmitter<{
if (root === null) {
return [];
}
if (!this.supportsTogglingSuspense(root.id)) {
if (!this.supportsTogglingSuspense(rootID)) {
return [];
}
const list: SuspenseNode['id'][] = [];
const suspense = this.getSuspenseByID(root.id);
const suspense = this.getSuspenseByID(rootID);
if (suspense !== null) {
const stack = [suspense];
while (stack.length > 0) {

View File

@@ -2,14 +2,40 @@
padding: .25rem;
}
.SuspenseRect {
fill: transparent;
stroke: var(--color-background-selected);
stroke-width: 1px;
vector-effect: non-scaling-stroke;
paint-order: stroke;
.SuspenseRectsViewBox {
position: relative;
}
[data-highlighted='true'] > .SuspenseRect {
fill: var(--color-selected-tree-highlight-active);
.SuspenseRectsBoundary {
pointer-events: all;
}
.SuspenseRectsBoundaryChildren {
pointer-events: none;
/**
* So that the shadow of Boundaries within is clipped off.
* Otherwise it would look like this boundary is further elevated.
*/
overflow: hidden;
}
.SuspenseRectsRect {
box-shadow: var(--elevation-4);
pointer-events: all;
outline-style: solid;
outline-width: 1px;
}
.SuspenseRectsScaledRect {
position: absolute;
outline-color: var(--color-background-selected);
}
/* highlight this boundary */
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
background-color: var(--color-background-hover);
}
.SuspenseRectsRect[data-highlighted='true'] {
background-color: var(--color-selected-tree-highlight-active);
}

View File

@@ -12,9 +12,13 @@ import type {
SuspenseNode,
Rect,
} from 'react-devtools-shared/src/frontend/types';
import typeof {
SyntheticMouseEvent,
SyntheticPointerEvent,
} from 'react-dom-bindings/src/events/SyntheticEvent';
import * as React from 'react';
import {useContext} from 'react';
import {createContext, useContext} from 'react';
import {
TreeDispatcherContext,
TreeStateContext,
@@ -26,19 +30,32 @@ import {
SuspenseTreeStateContext,
SuspenseTreeDispatcherContext,
} from './SuspenseTreeContext';
import typeof {
SyntheticMouseEvent,
SyntheticPointerEvent,
} from 'react-dom-bindings/src/events/SyntheticEvent';
function SuspenseRect({rect}: {rect: Rect}): React$Node {
function ScaledRect({
className,
rect,
...props
}: {
className: string,
rect: Rect,
...
}): React$Node {
const viewBox = useContext(ViewBox);
const width = (rect.width / viewBox.width) * 100 + '%';
const height = (rect.height / viewBox.height) * 100 + '%';
const x = ((rect.x - viewBox.x) / viewBox.width) * 100 + '%';
const y = ((rect.y - viewBox.y) / viewBox.height) * 100 + '%';
return (
<rect
className={styles.SuspenseRect}
x={rect.x}
y={rect.y}
width={rect.width}
height={rect.height}
<div
{...props}
className={styles.SuspenseRectsScaledRect + ' ' + className}
style={{
width,
height,
top: y,
left: x,
}}
/>
);
}
@@ -97,24 +114,67 @@ function SuspenseRects({
// TODO: Use the nearest Suspense boundary
const selected = inspectedElementID === suspenseID;
const boundingBox = getBoundingBox(suspense.rects);
return (
<g
data-highlighted={selected}
onClick={handleClick}
onPointerOver={handlePointerOver}
onPointerLeave={handlePointerLeave}>
<title>{suspense.name}</title>
{suspense.rects !== null &&
suspense.rects.map((rect, index) => {
return <SuspenseRect key={index} rect={rect} />;
})}
{suspense.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
})}
</g>
<ScaledRect rect={boundingBox} className={styles.SuspenseRectsBoundary}>
<ViewBox.Provider value={boundingBox}>
{suspense.rects !== null &&
suspense.rects.map((rect, index) => {
return (
<ScaledRect
key={index}
className={styles.SuspenseRectsRect}
rect={rect}
data-highlighted={selected}
onClick={handleClick}
onPointerOver={handlePointerOver}
onPointerLeave={handlePointerLeave}
// Reach-UI tooltip will go out of bounds of parent scroll container.
title={suspense.name}
/>
);
})}
{suspense.children.length > 0 && (
<ScaledRect
className={styles.SuspenseRectsBoundaryChildren}
rect={boundingBox}>
{suspense.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
})}
</ScaledRect>
)}
</ViewBox.Provider>
</ScaledRect>
);
}
function getBoundingBox(rects: $ReadOnlyArray<Rect> | null): Rect {
if (rects === null || rects.length === 0) {
return {x: 0, y: 0, width: 0, height: 0};
}
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
function getDocumentBoundingRect(
store: Store,
roots: $ReadOnlyArray<SuspenseNode['id']>,
@@ -169,42 +229,42 @@ function SuspenseRectsShell({
const store = useContext(StoreContext);
const root = store.getSuspenseByID(rootID);
if (root === null) {
console.warn(`<Element> Could not find suspense node id ${rootID}`);
// getSuspenseByID will have already warned
return null;
}
return (
<g>
{root.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
})}
</g>
);
return root.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
});
}
const ViewBox = createContext<Rect>((null: any));
function SuspenseRectsContainer(): React$Node {
const store = useContext(StoreContext);
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
const {roots} = useContext(SuspenseTreeStateContext);
const boundingRect = getDocumentBoundingRect(store, roots);
const boundingBox = getDocumentBoundingRect(store, roots);
const boundingBoxWidth = boundingBox.width;
const heightScale =
boundingBoxWidth === 0 ? 1 : boundingBox.height / boundingBoxWidth;
// Scales the inspected document to fit into the available width
const width = '100%';
const boundingRectWidth = boundingRect.width;
const height =
(boundingRectWidth === 0 ? 0 : boundingRect.height / boundingRect.width) *
100 +
'%';
const aspectRatio = `1 / ${heightScale}`;
return (
<div className={styles.SuspenseRectsContainer}>
<svg
style={{width, height}}
viewBox={`${boundingRect.x} ${boundingRect.y} ${boundingRect.width} ${boundingRect.height}`}>
{roots.map(rootID => {
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
})}
</svg>
<ViewBox.Provider value={boundingBox}>
<div
className={styles.SuspenseRectsViewBox}
style={{aspectRatio, width}}>
{roots.map(rootID => {
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
})}
</div>
</ViewBox.Provider>
</div>
);
}

View File

@@ -125,6 +125,7 @@ import {
enableViewTransition,
enableHydrationChangeEvent,
enableFragmentRefsScrollIntoView,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
@@ -2098,6 +2099,8 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
@@ -2131,10 +2134,10 @@ export function startViewTransition(
blockingPromises.push(ownerDocument.fonts.ready);
}
}
const blockingIndexSnapshot = blockingPromises.length;
if (suspendedState !== null) {
// Suspend on any images that still haven't loaded and are in the viewport.
const suspenseyImages = suspendedState.suspenseyImages;
const blockingIndexSnapshot = blockingPromises.length;
let imgBytes = 0;
for (let i = 0; i < suspenseyImages.length; i++) {
const suspenseyImage = suspenseyImages[i];
@@ -2162,6 +2165,15 @@ export function startViewTransition(
}
}
if (blockingPromises.length > 0) {
if (enableProfilerTimer) {
const blockedReason =
blockingIndexSnapshot > 0
? blockingPromises.length > blockingIndexSnapshot
? 'Waiting on Fonts and Images'
: 'Waiting on Fonts'
: 'Waiting on Images';
blockedCallback(blockedReason);
}
const blockingReady = Promise.race([
Promise.all(blockingPromises),
new Promise(resolve =>
@@ -2291,6 +2303,9 @@ export function startViewTransition(
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
finishedAnimation();
}
passiveCallback();
});
return transition;
@@ -5954,6 +5969,7 @@ export opaque type SuspendedState = {
imgBytes: number, // number of bytes we estimate needing to download
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
waitingForImages: boolean, // false when we're no longer blocking on images
waitingForViewTransition: boolean,
unsuspend: null | (() => void),
};
@@ -5965,6 +5981,7 @@ export function startSuspendingCommit(): SuspendedState {
imgBytes: 0,
suspenseyImages: [],
waitingForImages: true,
waitingForViewTransition: false,
// We use a noop function when we begin suspending because if possible we want the
// waitfor step to finish synchronously. If it doesn't we'll return a function to
// provide the actual unsuspend function and that will get completed when the count
@@ -6112,6 +6129,7 @@ export function suspendOnActiveViewTransition(
return;
}
state.count++;
state.waitingForViewTransition = true;
const ping = onUnsuspend.bind(state);
activeViewTransition.finished.then(ping, ping);
}
@@ -6195,6 +6213,28 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
if (state.waitingForViewTransition) {
return 'Waiting for the previous Animation';
}
if (state.count > 0) {
if (state.imgCount > 0) {
return 'Suspended on CSS and Images';
}
return 'Suspended on CSS';
}
if (state.imgCount === 1) {
return 'Suspended on an Image';
}
if (state.imgCount > 0) {
return 'Suspended on Images';
}
return null;
}
function checkIfFullyUnsuspended(state: SuspendedState) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) {

View File

@@ -627,6 +627,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export type FragmentInstanceType = {
_fragmentFiber: Fiber,
_observers: null | Set<IntersectionObserver>,

View File

@@ -673,6 +673,8 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
@@ -805,6 +807,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
waitForCommitToBeReady,
getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
},
NotPendingTransition: (null: TransitionStatus),
resetFormInstance(form: Instance) {},
@@ -853,6 +860,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();

View File

@@ -39,6 +39,11 @@ import {
getViewTransitionName,
getViewTransitionClassName,
} from './ReactFiberViewTransitionComponent';
import {trackAnimatingTask} from './ReactProfilerTimer';
import {
enableComponentPerformanceTrack,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
export let shouldStartViewTransition: boolean = false;
@@ -101,21 +106,27 @@ export function popViewTransitionCancelableScope(
let viewTransitionHostInstanceIdx = 0;
export function applyViewTransitionToHostInstances(
child: null | Fiber,
function applyViewTransitionToHostInstances(
fiber: Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
return applyViewTransitionToHostInstancesRecursive(
child,
const inViewport = applyViewTransitionToHostInstancesRecursive(
fiber.child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
);
if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
if (fiber._debugTask != null) {
trackAnimatingTask(fiber._debugTask);
}
}
return inViewport;
}
function applyViewTransitionToHostInstancesRecursive(
@@ -247,7 +258,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
// We found a new appearing view transition with the same name as this deletion.
// We'll transition between them.
const inViewport = applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
null,
@@ -284,7 +295,7 @@ export function commitEnterViewTransitions(
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
placement.child,
placement,
name,
className,
null,
@@ -355,7 +366,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
if (className !== 'none') {
// We found a new appearing view transition with the same name as this deletion.
const inViewport = applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
null,
@@ -406,7 +417,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
deletion.child,
deletion,
name,
className,
null,
@@ -490,7 +501,7 @@ export function commitBeforeUpdateViewTransition(
return;
}
applyViewTransitionToHostInstances(
current.child,
current,
oldName,
className,
(current.memoizedState = []),
@@ -518,7 +529,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
child.flags &= ~Update;
if (className !== 'none') {
applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
(child.memoizedState = []),

View File

@@ -73,6 +73,8 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
export const SomeTransitionLane: Lane = TransitionLane1;
const TransitionUpdateLanes =
TransitionLane1 |
TransitionLane2 |
@@ -633,6 +635,22 @@ export function includesTransitionLane(lanes: Lanes): boolean {
return (lanes & TransitionLanes) !== NoLanes;
}
export function includesRetryLane(lanes: Lanes): boolean {
return (lanes & RetryLanes) !== NoLanes;
}
export function includesIdleGroupLanes(lanes: Lanes): boolean {
return (
(lanes &
(SelectiveHydrationLane |
IdleHydrationLane |
IdleLane |
OffscreenLane |
DeferredLane)) !==
NoLanes
);
}
export function includesOnlyHydrationLanes(lanes: Lanes): boolean {
return (lanes & HydrationLanes) === lanes;
}

View File

@@ -1180,45 +1180,10 @@ export function logInconsistentRender(
}
}
export function logSuspenseThrottlePhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
// This was inside a throttled Suspense boundary commit.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
export function logSuspendedCommitPhase(
startTime: number,
endTime: number,
reason: string,
debugTask: null | ConsoleTask,
): void {
// This means the commit was suspended on CSS or images.
@@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,
@@ -1243,7 +1208,46 @@ export function logSuspendedCommitPhase(
);
} else {
console.timeStamp(
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
export function logSuspendedViewTransitionPhase(
startTime: number,
endTime: number,
reason: string,
debugTask: null | ConsoleTask,
): void {
// This means the commit was suspended on CSS or images.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
// TODO: Include the exact reason and URLs of what resources suspended.
// TODO: This might also be Suspended while waiting on a View Transition.
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
reason,
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
reason,
startTime,
endTime,
currentTrack,
@@ -1454,7 +1458,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
),
);
} else {
@@ -1464,7 +1468,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
);
}
}

View File

@@ -79,8 +79,8 @@ import {
logErroredRenderPhase,
logInconsistentRender,
logSuspendedWithDelayPhase,
logSuspenseThrottlePhase,
logSuspendedCommitPhase,
logSuspendedViewTransitionPhase,
logCommitPhase,
logPaintYieldPhase,
logStartViewTransitionYieldPhase,
@@ -102,6 +102,7 @@ import {
startSuspendingCommit,
suspendOnActiveViewTransition,
waitForCommitToBeReady,
getSuspendedCommitReason,
preloadInstance,
preloadResource,
supportsHydration,
@@ -178,6 +179,8 @@ import {
includesOnlyTransitions,
includesBlockingLane,
includesTransitionLane,
includesRetryLane,
includesIdleGroupLanes,
includesExpiredLane,
getNextLanes,
getEntangledLanes,
@@ -200,6 +203,9 @@ import {
includesOnlyViewTransitionEligibleLanes,
isGestureRender,
GestureLane,
SomeTransitionLane,
SomeRetryLane,
IdleLane,
} from './ReactFiberLane';
import {
DiscreteEventPriority,
@@ -291,6 +297,8 @@ import {
clearTransitionTimers,
clampBlockingTimers,
clampTransitionTimers,
clampRetryTimers,
clampIdleTimers,
markNestedUpdateScheduled,
renderStartTime,
commitStartTime,
@@ -311,6 +319,12 @@ import {
resetCommitErrors,
PINGED_UPDATE,
SPAWNED_UPDATE,
startAnimating,
stopAnimating,
animatingLanes,
retryClampTime,
idleClampTime,
animatingTask,
} from './ReactProfilerTimer';
// DEV stuff
@@ -671,12 +685,10 @@ export function getRenderTargetTime(): number {
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
type SuspendedCommitReason = 0 | 1 | 2;
const IMMEDIATE_COMMIT = 0;
const SUSPENDED_COMMIT = 1;
const THROTTLED_COMMIT = 2;
type SuspendedCommitReason = null | string;
type DelayedCommitReason = 0 | 1 | 2 | 3;
const IMMEDIATE_COMMIT = 0;
const ABORTED_VIEW_TRANSITION_COMMIT = 1;
const DELAYED_PASSIVE_COMMIT = 2;
const ANIMATION_STARTED_COMMIT = 3;
@@ -702,8 +714,9 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null;
let pendingTransitionTypes: null | TransitionTypes = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
// Use these to prevent an infinite loop of nested updates
const NESTED_UPDATE_LIMIT = 50;
@@ -1389,7 +1402,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
exitStatus,
null,
IMMEDIATE_COMMIT,
null,
renderStartTime,
renderEndTime,
);
@@ -1426,6 +1439,7 @@ function finishConcurrentRender(
// immediately, wait for more data to arrive.
// TODO: Combine retry throttling with Suspensey commits. Right now they
// run one after the other.
pendingEffectsLanes = lanes;
root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
@@ -1440,7 +1454,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
THROTTLED_COMMIT,
'Throttled',
renderStartTime,
renderEndTime,
),
@@ -1461,7 +1475,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
IMMEDIATE_COMMIT,
null,
renderStartTime,
renderEndTime,
);
@@ -1539,6 +1553,7 @@ function commitRootWhenReady(
// Not yet ready to commit. Delay the commit until the renderer notifies
// us that it's ready. This will be canceled if we start work on the
// root again.
pendingEffectsLanes = lanes;
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(
null,
@@ -1553,7 +1568,9 @@ function commitRootWhenReady(
suspendedRetryLanes,
exitStatus,
suspendedState,
SUSPENDED_COMMIT,
enableProfilerTimer
? getSuspendedCommitReason(suspendedState, root.containerInfo)
: null,
completedRenderStartTime,
completedRenderEndTime,
),
@@ -1887,6 +1904,12 @@ function finalizeRender(lanes: Lanes, finalizationTime: number): void {
if (includesTransitionLane(lanes)) {
clampTransitionTimers(finalizationTime);
}
if (includesRetryLane(lanes)) {
clampRetryTimers(finalizationTime);
}
if (includesIdleGroupLanes(lanes)) {
clampIdleTimers(finalizationTime);
}
}
}
@@ -1937,6 +1960,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
}
finalizeRender(workInProgressRootRenderLanes, renderStartTime);
}
const previousUpdateTask = workInProgressUpdateTask;
workInProgressUpdateTask = null;
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
@@ -1949,18 +1973,30 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
blockingEventTime >= 0 && blockingEventTime < blockingClampTime
? blockingClampTime
: blockingEventTime;
const clampedRenderStartTime = // Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (blockingSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SyncLane);
logSuspendedWithDelayPhase(
blockingSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
previousUpdateTask,
);
} else if (
includesSyncLane(animatingLanes) ||
includesBlockingLane(animatingLanes)
) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(
blockingClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logBlockingStart(
@@ -1992,19 +2028,29 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
transitionEventTime >= 0 && transitionEventTime < transitionClampTime
? transitionClampTime
: transitionEventTime;
const clampedRenderStartTime =
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (transitionSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SomeTransitionLane);
logSuspendedWithDelayPhase(
transitionSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
);
} else if (includesTransitionLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(
transitionClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logTransitionStart(
clampedStartTime,
@@ -2020,6 +2066,20 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
clearTransitionTimers();
}
if (includesRetryLane(lanes)) {
if (includesRetryLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, renderStartTime, animatingTask);
}
}
if (includesIdleGroupLanes(lanes)) {
if (includesIdleGroupLanes(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, renderStartTime, animatingTask);
}
}
}
const timeoutHandle = root.timeoutHandle;
@@ -2036,6 +2096,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
cancelPendingCommit();
}
pendingEffectsLanes = NoLanes;
resetWorkInProgressStack();
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
@@ -3445,6 +3507,7 @@ function commitRoot(
pendingEffectsRenderEndTime = completedRenderEndTime;
pendingSuspendedCommitReason = suspendedCommitReason;
pendingDelayedCommitReason = IMMEDIATE_COMMIT;
pendingSuspendedViewTransitionReason = null;
}
if (enableGestureTransition && isGestureRender(lanes)) {
@@ -3455,7 +3518,7 @@ function commitRoot(
recoverableErrors,
suspendedState,
enableProfilerTimer
? suspendedCommitReason === IMMEDIATE_COMMIT
? suspendedCommitReason === null
? completedRenderEndTime
: commitStartTime
: 0,
@@ -3527,16 +3590,11 @@ function commitRoot(
resetCommitErrors();
recordCommitTime();
if (enableComponentPerformanceTrack) {
if (suspendedCommitReason === SUSPENDED_COMMIT) {
if (suspendedCommitReason !== null) {
logSuspendedCommitPhase(
completedRenderEndTime,
commitStartTime,
workInProgressUpdateTask,
);
} else if (suspendedCommitReason === THROTTLED_COMMIT) {
logSuspenseThrottlePhase(
completedRenderEndTime,
commitStartTime,
suspendedCommitReason,
workInProgressUpdateTask,
);
}
@@ -3594,6 +3652,9 @@ function commitRoot(
pendingEffectsStatus = PENDING_MUTATION_PHASE;
if (enableViewTransition && willStartViewTransition) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
startAnimating(lanes);
}
pendingViewTransition = startViewTransition(
suspendedState,
root.containerInfo,
@@ -3604,6 +3665,11 @@ function commitRoot(
flushSpawnedWork,
flushPassiveEffects,
reportViewTransitionError,
enableProfilerTimer ? suspendedViewTransition : (null: any),
enableProfilerTimer
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
finishedViewTransition.bind(null, lanes)
: (null: any),
);
} else {
// Flush synchronously.
@@ -3624,6 +3690,71 @@ function reportViewTransitionError(error: mixed) {
onRecoverableError(error, makeErrorInfo(null));
}
function suspendedViewTransition(reason: string): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// We'll split the commit into two phases, because we're suspended in the middle.
recordCommitEndTime();
logCommitPhase(
pendingSuspendedCommitReason === null
? pendingEffectsRenderEndTime
: commitStartTime,
commitEndTime,
commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
);
pendingSuspendedViewTransitionReason = reason;
pendingSuspendedCommitReason = reason;
}
}
function finishedViewTransition(lanes: Lanes): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if ((animatingLanes & lanes) === NoLanes) {
// Was already stopped by some other action or maybe other root.
return;
}
const task = animatingTask;
stopAnimating(lanes);
// If an affected track isn't in the middle of rendering or committing, log from the previous
// finished render until the end of the animation.
if (
(includesSyncLane(lanes) || includesBlockingLane(lanes)) &&
!includesSyncLane(workInProgressRootRenderLanes) &&
!includesBlockingLane(workInProgressRootRenderLanes) &&
!includesSyncLane(pendingEffectsLanes) &&
!includesBlockingLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(blockingClampTime, now(), task);
}
if (
includesTransitionLane(lanes) &&
!includesTransitionLane(workInProgressRootRenderLanes) &&
!includesTransitionLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(transitionClampTime, now(), task);
}
if (
includesRetryLane(lanes) &&
!includesRetryLane(workInProgressRootRenderLanes) &&
!includesRetryLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, now(), task);
}
if (
includesIdleGroupLanes(lanes) &&
!includesIdleGroupLanes(workInProgressRootRenderLanes) &&
!includesIdleGroupLanes(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, now(), task);
}
}
}
function flushAfterMutationEffects(): void {
if (pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE) {
return;
@@ -3688,6 +3819,21 @@ function flushLayoutEffects(): void {
}
pendingEffectsStatus = NO_PENDING_EFFECTS;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
const suspendedViewTransitionReason = pendingSuspendedViewTransitionReason;
if (suspendedViewTransitionReason !== null) {
// We suspended in the middle of the commit for the view transition.
// We'll start a new commit track now.
recordCommitTime();
logSuspendedViewTransitionPhase(
commitEndTime, // The start is the end of the first commit part.
commitStartTime, // The end is the start of the second commit part.
suspendedViewTransitionReason,
animatingTask,
);
}
}
const root = pendingEffectsRoot;
const finishedWork = pendingFinishedWork;
const lanes = pendingEffectsLanes;
@@ -3755,9 +3901,7 @@ function flushLayoutEffects(): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
recordCommitEndTime();
logCommitPhase(
suspendedCommitReason === IMMEDIATE_COMMIT
? completedRenderEndTime
: commitStartTime,
suspendedCommitReason === null ? completedRenderEndTime : commitStartTime,
commitEndTime,
commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
@@ -3788,7 +3932,7 @@ function flushSpawnedWork(): void {
startViewTransitionStartTime,
commitEndTime,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task.
animatingTask,
);
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
@@ -4290,11 +4434,7 @@ function flushPassiveEffectsImpl() {
passiveEffectStartTime = now();
if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
// The animation was started, so we've been animating since that happened.
logAnimatingPhase(
commitEndTime,
passiveEffectStartTime,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task
);
logAnimatingPhase(commitEndTime, passiveEffectStartTime, animatingTask);
} else {
logPaintYieldPhase(
commitEndTime,

View File

@@ -22,6 +22,7 @@ import {
includesTransitionLane,
includesBlockingLane,
includesSyncLane,
NoLanes,
} from './ReactFiberLane';
import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig';
@@ -88,6 +89,12 @@ export let transitionEventType: null | string = null; // Event type of the first
export let transitionEventIsRepeat: boolean = false;
export let transitionSuspendedTime: number = -1.1;
export let retryClampTime: number = -0;
export let idleClampTime: number = -0;
export let animatingLanes: Lanes = NoLanes;
export let animatingTask: null | ConsoleTask = null; // First ViewTransition applying an Animation.
export let yieldReason: SuspendedReason = (0: any);
export let yieldStartTime: number = -1.1; // The time when we yielded to the event loop
@@ -306,6 +313,20 @@ export function clampTransitionTimers(finalTime: number): void {
transitionClampTime = finalTime;
}
export function clampRetryTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
retryClampTime = finalTime;
}
export function clampIdleTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
idleClampTime = finalTime;
}
export function pushNestedEffectDurations(): number {
if (!enableProfilerTimer || !enableProfilerCommitHooks) {
return 0;
@@ -578,3 +599,19 @@ export function transferActualDuration(fiber: Fiber): void {
child = child.sibling;
}
}
export function startAnimating(lanes: Lanes): void {
animatingLanes |= lanes;
animatingTask = null;
}
export function stopAnimating(lanes: Lanes): void {
animatingLanes &= ~lanes;
animatingTask = null;
}
export function trackAnimatingTask(task: ConsoleTask): void {
if (animatingTask === null) {
animatingTask = task;
}
}

View File

@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
waitForCommitToBeReady(state, timeoutOffset) {
return null;
},
getSuspendedCommitReason(state, rootContainer) {
return null;
},
supportsMutation: true,
});

View File

@@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
export const suspendOnActiveViewTransition =
$$$config.suspendOnActiveViewTransition;
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
export const NotPendingTransition = $$$config.NotPendingTransition;
export const HostTransitionContext = $$$config.HostTransitionContext;
export const resetFormInstance = $$$config.resetFormInstance;

View File

@@ -132,7 +132,19 @@ export function resolveServerReference<T>(
);
}
}
// TODO: This needs to return async: true if it's an async module.
if (resolvedModuleData.async) {
// If the module is marked as async in a Client Reference, we don't actually care.
// What matters is whether the consumer wants to unwrap it or not.
// For Server References, it is different because the consumer is completely internal
// to the bundler. So instead of passing it to each reference we can mark it in the
// manifest.
return [
resolvedModuleData.id,
resolvedModuleData.chunks,
name,
1 /* async */,
];
}
return [resolvedModuleData.id, resolvedModuleData.chunks, name];
}

View File

@@ -4702,6 +4702,70 @@ function renderDebugModel(
element._store.validated,
];
}
case REACT_LAZY_TYPE: {
// To avoid actually initializing a lazy causing a side-effect, we make
// some assumptions about the structure of the payload even though
// that's not really part of the contract. In practice, this is really
// just coming from React.lazy helper or Flight.
const lazy: LazyComponent<any, any> = (value: any);
const payload = lazy._payload;
if (payload !== null && typeof payload === 'object') {
// React.lazy constructor
switch (payload._status) {
case -1 /* Uninitialized */:
case 0 /* Pending */:
break;
case 1 /* Resolved */: {
const id = outlineDebugModel(request, counter, payload._result);
return serializeLazyID(id);
}
case 2 /* Rejected */: {
// We don't log these errors since they didn't actually throw into
// Flight.
const digest = '';
const id = request.nextChunkId++;
emitErrorChunk(request, id, digest, payload._result, true, null);
return serializeLazyID(id);
}
}
// React Flight
switch (payload.status) {
case 'pending':
case 'blocked':
case 'resolved_model':
// The value is an uninitialized model from the Flight client.
// It's not very useful to emit that.
break;
case 'resolved_module':
// The value is client reference metadata from the Flight client.
// It's likely for SSR, so we choose not to emit it.
break;
case 'fulfilled': {
const id = outlineDebugModel(request, counter, payload.value);
return serializeLazyID(id);
}
case 'rejected': {
// We don't log these errors since they didn't actually throw into
// Flight.
const digest = '';
const id = request.nextChunkId++;
emitErrorChunk(request, id, digest, payload.reason, true, null);
return serializeLazyID(id);
}
}
}
// We couldn't emit a resolved or rejected value synchronously. For now,
// we emit this as a halted chunk. TODO: We could maybe also handle
// pending lazy debug models like we do in serializeDebugThenable,
// if/when we determine that it's worth the added complexity.
request.pendingDebugChunks++;
const id = request.nextChunkId++;
emitDebugHaltChunk(request, id);
return serializeLazyID(id);
}
}
// $FlowFixMe[method-unbinding]

File diff suppressed because it is too large Load Diff

View File

@@ -423,6 +423,8 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
@@ -588,6 +590,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -37,7 +37,7 @@ The high level process of creating releases is [documented below](#process). Ind
If your code lands in the main branch, it will be automatically published to the prerelease channels within the next weekday. However, if you want to immediately publish a prerelease, you can trigger the job to run immediately via the GitHub UI:
1. Wait for the commit you want to release to finish its [(Runtime) Build and Test workflow](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml), as the prerelease script needs to download the build from that workflow.
2. Copy the git sha of whichever commit you are trying to release
2. Copy the full git sha of whichever commit you are trying to release
3. Go to https://github.com/facebook/react/actions/workflows/runtime_prereleases_manual.yml
4. Paste the git sha into the "Run workflow" dropdown
5. Let the job finish and it will be released on npm