Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ee7b65c71 | ||
|
|
d966edf822 | ||
|
|
720bb13069 | ||
|
|
1eca9a2747 | ||
|
|
cd85bb5616 | ||
|
|
07e4974bad | ||
|
|
d91d28c8ba | ||
|
|
b4fe1e6c7e | ||
|
|
b204edda3a | ||
|
|
115e3ec15f | ||
|
|
565eb7888e | ||
|
|
d415fd3ed7 | ||
|
|
5e3cd53f20 | ||
|
|
01cad9eaca | ||
|
|
6eda534718 | ||
|
|
c03a51d836 | ||
|
|
ad578aa01f | ||
|
|
03a96c75db | ||
|
|
755cebad6b | ||
|
|
581321160f | ||
|
|
1bcdd224b1 |
4
.github/workflows/runtime_build_and_test.yml
vendored
4
.github/workflows/runtime_build_and_test.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
acdlite
|
||||
eps1lon
|
||||
EugeneChoi4
|
||||
gaearon
|
||||
gnoff
|
||||
unstubbable
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies Partial<PluginOptions>);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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('');
|
||||
|
||||
@@ -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;
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,4 +29,6 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
2
compiler/apps/playground/next-env.d.ts
vendored
2
compiler/apps/playground/next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
const testComplexPluginOptionDefaults: PluginOptions = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -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}],
|
||||
};
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 (
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 (
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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}</>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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: () => {}}],
|
||||
};
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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: ']'}],
|
||||
};
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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'}}],
|
||||
};
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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}],
|
||||
};
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
@@ -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: [{}],
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export {
|
||||
} from './ReactiveScopes';
|
||||
export {parseConfigPragmaForTests} from './Utils/TestUtils';
|
||||
declare global {
|
||||
// @internal
|
||||
let __DEV__: boolean | null | undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
131
packages/internal-test-utils/debugInfo.js
Normal file
131
packages/internal-test-utils/debugInfo.js
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
71
packages/react-client/src/ReactFlightClient.js
vendored
71
packages/react-client/src/ReactFlightClient.js
vendored
@@ -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 &&
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = []),
|
||||
|
||||
18
packages/react-reconciler/src/ReactFiberLane.js
vendored
18
packages/react-reconciler/src/ReactFiberLane.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
224
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
224
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
|
||||
waitForCommitToBeReady(state, timeoutOffset) {
|
||||
return null;
|
||||
},
|
||||
getSuspendedCommitReason(state, rootContainer) {
|
||||
return null;
|
||||
},
|
||||
supportsMutation: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
64
packages/react-server/src/ReactFlightServer.js
vendored
64
packages/react-server/src/ReactFlightServer.js
vendored
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user