Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e67b0e03e | ||
|
|
648c5045c4 | ||
|
|
04927c2ee7 | ||
|
|
1d9ccd1d1b | ||
|
|
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, {
|
||||
|
||||
@@ -520,7 +520,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender:
|
||||
case ErrorCategory.EffectDerivationsOfState:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
@@ -614,10 +614,7 @@ export enum ErrorCategory {
|
||||
* Checks for no setState in effect bodies
|
||||
*/
|
||||
EffectSetState = 'EffectSetState',
|
||||
/**
|
||||
* Checks for no deriving state in effects, solved by calculate in render
|
||||
*/
|
||||
EffectStateDerivationCalculateInRender = 'EffectStateDerivationCalculateInRender',
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
/**
|
||||
* Validates against try/catch in place of error boundaries
|
||||
*/
|
||||
@@ -754,11 +751,11 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectStateDerivationCalculateInRender: {
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'no-deriving-state-in-effects-calculate-in-render',
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
@@ -275,6 +276,10 @@ function runWithEnvironment(
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -334,6 +334,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
|
||||
* during render. Generates a custom error message for each type of violation.
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
|
||||
@@ -1816,8 +1816,16 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
/**
|
||||
* Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
|
||||
* type for the object. In some cases the variable may be named like a ref, but is
|
||||
* also used as a ref callback such that we infer the type as a function rather than
|
||||
* a ref.
|
||||
*/
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
value.kind === 'PropertyStore' &&
|
||||
value.property === 'current' &&
|
||||
value.object.identifier.type.kind === 'Type'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
|
||||
@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
const testComplexPluginOptionDefaults: PluginOptions = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
|
||||
@@ -5,296 +5,142 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
BasicBlock,
|
||||
GeneratedSource,
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
isSetStateType,
|
||||
Place,
|
||||
isUseStateType,
|
||||
Effect,
|
||||
isUseEffectHookType,
|
||||
FunctionExpression,
|
||||
BlockId,
|
||||
SourceLocation,
|
||||
CallExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type DerivationCache = Map<IdentifierId, DerivationMetadata>;
|
||||
|
||||
type SetStateCallCache = Map<string | undefined | null, Array<Place>>;
|
||||
|
||||
type FunctionExpressionsCache = Map<IdentifierId, FunctionExpression>;
|
||||
|
||||
type DerivedSetStateCall = {
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
derivedComputationDetails: string;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
const DERIVE_IN_RENDER_REASON =
|
||||
'You might net need an effect. Derive values in render, not effects.';
|
||||
|
||||
const DERIVE_IN_RENDER_DETAIL_MESSAGE =
|
||||
'This should be computed during render, not in an effect';
|
||||
|
||||
const DERIVE_IN_RENDER_DESCRIPTION =
|
||||
'State derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user';
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const derivationCache: DerivationCache = new Map();
|
||||
const setStateCallCache: SetStateCallCache = new Map();
|
||||
const effectSetStateCache: SetStateCallCache = new Map();
|
||||
const functionExpressionsCache: FunctionExpressionsCache = new Map();
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const stateDerivationErrors: Array<ErrorMetadata> = [];
|
||||
|
||||
parseFNParameters(fn, derivationCache);
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerErrors(stateDerivationErrors);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFNParameters(fn: HIRFunction, derivationCache: DerivationCache) {
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: DerivationCache,
|
||||
): 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'
|
||||
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'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
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'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'this is checked as a place above',
|
||||
},
|
||||
],
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(derivedVar.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
functionExpressionsCache: FunctionExpressionsCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
|
||||
// Recursively parse function expressions
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
functionExpressionsCache.set(lvalue.identifier.id, value);
|
||||
|
||||
parseInstr(
|
||||
instr,
|
||||
derivationCache,
|
||||
setStateCallCache,
|
||||
effectSetStateCache,
|
||||
functionExpressionsCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record setState calls
|
||||
else if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier)
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, setStateCallCache);
|
||||
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
// Handle values derived from useState calls
|
||||
if (isUseStateType(lvalue.identifier)) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
// Validate useEffect calls
|
||||
else if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functionExpressionsCache.get(
|
||||
value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
validateEffect(
|
||||
effectFunction?.loweredFunc.func,
|
||||
effectSetStateCache,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parseOperands(instr, derivationCache, typeOfValue, sources);
|
||||
}
|
||||
|
||||
function addSetStateCallEntry(
|
||||
callee: Place,
|
||||
setStateCallCache: SetStateCallCache,
|
||||
) {
|
||||
if (callee.loc === GeneratedSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (setStateCallCache.has(callee.loc.identifierName)) {
|
||||
setStateCallCache.get(callee.loc.identifierName)!.push(callee);
|
||||
} else {
|
||||
setStateCallCache.set(callee.loc.identifierName, [callee]);
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction | undefined,
|
||||
effectSetStateCache: SetStateCallCache,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
if (effectFunction === undefined) {
|
||||
return;
|
||||
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 effectDerivedSetStateCalls: Array<DerivedSetStateCall> = [];
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -302,183 +148,91 @@ 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) {
|
||||
const {value} = instr;
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isSetStateType(value.callee.identifier) &&
|
||||
value.args.length === 1 &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
addSetStateCallEntry(value.callee, effectSetStateCache);
|
||||
const argMetadata = derivationCache.get(value.args[0].identifier.id);
|
||||
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 (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
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;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls,
|
||||
derivationCache,
|
||||
stateDerivationErrors,
|
||||
);
|
||||
}
|
||||
|
||||
function generateDerivedComputationDetails(
|
||||
effectDerivedSetStateCalls: Array<DerivedSetStateCall>,
|
||||
derivationCache: DerivationCache,
|
||||
stateDerivationErrors: Array<ErrorMetadata>,
|
||||
) {
|
||||
console.log(derivationCache);
|
||||
for (const derivedCall of effectDerivedSetStateCalls) {
|
||||
const arg = derivedCall.value.args[0];
|
||||
if (arg.kind === 'Identifier') {
|
||||
const argMetadata = derivationCache.get(arg.identifier.id);
|
||||
if (argMetadata !== undefined) {
|
||||
const derivationSources: Array<string> = [];
|
||||
|
||||
for (const sourceId of argMetadata.sourcesIds) {
|
||||
const sourceMetadata = derivationCache.get(sourceId);
|
||||
if (sourceMetadata !== undefined) {
|
||||
const sourceName =
|
||||
sourceMetadata.place.identifier.name?.value ||
|
||||
`identifier_${sourceId}`;
|
||||
derivationSources.push(sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
let derivationType: string;
|
||||
switch (argMetadata.typeOfValue) {
|
||||
case 'fromProps':
|
||||
derivationType = 'props';
|
||||
break;
|
||||
case 'fromState':
|
||||
derivationType = 'local state';
|
||||
break;
|
||||
case 'fromPropsAndState':
|
||||
derivationType = 'local state and props';
|
||||
break;
|
||||
default:
|
||||
derivationType = 'unknown source';
|
||||
break;
|
||||
}
|
||||
|
||||
const sourcesList =
|
||||
derivationSources.length > 0
|
||||
? ` [${derivationSources.join(', ')}]`
|
||||
: '';
|
||||
|
||||
const formattedDetails = `State is being derived from ${derivationType}${sourcesList}`;
|
||||
|
||||
stateDerivationErrors.push({
|
||||
derivedComputationDetails: formattedDetails,
|
||||
loc: derivedCall.value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const loc of setStateLocations) {
|
||||
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,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseOperands(
|
||||
instr: Instruction,
|
||||
derivationCache: DerivationCache,
|
||||
typeOfValue: TypeOfValue,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
) {
|
||||
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) {
|
||||
sourceIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
propagateTypeOfValue(instr, sourceIds, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
function propagateTypeOfValue(
|
||||
instr: Instruction,
|
||||
sourceIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: DerivationCache,
|
||||
): void {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
addDerivationEntry(lvalue, sourceIds, 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, sourceIds, 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: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerErrors(stateDerivationErrors: Array<ErrorMetadata>) {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const e of stateDerivationErrors) {
|
||||
throwableErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description:
|
||||
DERIVE_IN_RENDER_DESCRIPTION + `\n\n${e.derivedComputationDetails}`,
|
||||
category: ErrorCategory.EffectStateDerivationCalculateInRender,
|
||||
reason: DERIVE_IN_RENDER_REASON,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: e.loc,
|
||||
message: DERIVE_IN_RENDER_DETAIL_MESSAGE,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* 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 {CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
Place,
|
||||
CallExpression,
|
||||
Instruction,
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
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>;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
hasChanges: boolean = false;
|
||||
cache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
|
||||
snapshot(): boolean {
|
||||
const hasChanges = this.hasChanges;
|
||||
this.hasChanges = false;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = this.cache.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
const existingValue = this.cache.get(derivedVar.identifier.id);
|
||||
if (
|
||||
existingValue === undefined ||
|
||||
!this.isDerivationEqual(existingValue, newValue)
|
||||
) {
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
this.hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
private isDerivationEqual(
|
||||
a: DerivationMetadata,
|
||||
b: DerivationMetadata,
|
||||
): boolean {
|
||||
if (a.typeOfValue !== b.typeOfValue) {
|
||||
return false;
|
||||
}
|
||||
if (a.sourcesIds.size !== b.sourcesIds.size) {
|
||||
return false;
|
||||
}
|
||||
for (const id of a.sourcesIds) {
|
||||
if (!b.sourcesIds.has(id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context);
|
||||
}
|
||||
}
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function recordPhiDerivations(
|
||||
block: BasicBlock,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
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 = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
phi.place,
|
||||
sourcesIds,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context);
|
||||
}
|
||||
}
|
||||
} 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'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
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 = context.derivationCache.cache.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)) {
|
||||
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
|
||||
}
|
||||
|
||||
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)) {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
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: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
}> = [];
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const argMetadata = context.derivationCache.cache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
}
|
||||
} else if (instr.value.kind === 'CallExpression') {
|
||||
const calleeMetadata = context.derivationCache.cache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
calleeMetadata !== undefined &&
|
||||
(calleeMetadata.typeOfValue === 'fromProps' ||
|
||||
calleeMetadata.typeOfValue === 'fromPropsAndState')
|
||||
) {
|
||||
// If the callee is a prop we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
|
||||
if (globals.has(instr.value.callee.identifier.id)) {
|
||||
// If the callee is a global we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
} else if (instr.value.kind === 'LoadGlobal') {
|
||||
globals.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
globals.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
context.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: derivedSetStateCall.value.callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
setLocal(myRef.current + test);
|
||||
};
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -0,0 +1,19 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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,75 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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: () => {}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { propValue, onChange } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== onChange || $[1] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
};
|
||||
$[0] = onChange;
|
||||
$[1] = propValue;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== propValue) {
|
||||
t2 = [propValue];
|
||||
$[3] = propValue;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[5] = value;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test", onChange: () => {} }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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,70 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
};
|
||||
t2 = [propValue];
|
||||
$[0] = propValue;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[3] = value;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) globalCall is not defined
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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-local-state-in-effect.ts:10:6
|
||||
8 | useEffect(() => {
|
||||
9 | if (shouldChange) {
|
||||
> 10 | setCount(count + 1);
|
||||
| ^^^^^^^^ 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 | }
|
||||
12 | }, [count]);
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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_exp
|
||||
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,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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_exp
|
||||
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,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: 4 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 8
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
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,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
|
||||
13 | } else {
|
||||
14 | // So this assignment fails since we don't know its a ref
|
||||
> 15 | parentRef.current = instance;
|
||||
| ^^^^^^^^^ `parentRef` cannot be modified
|
||||
16 | }
|
||||
17 | }
|
||||
18 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @validateRefAccessDuringRender
|
||||
|
||||
function useHook(parentRef) {
|
||||
// Some components accept a union of "callback" refs and ref objects, which
|
||||
// we can't currently represent
|
||||
const elementRef = useRef(null);
|
||||
const handler = instance => {
|
||||
elementRef.current = instance;
|
||||
if (parentRef != null) {
|
||||
if (typeof parentRef === 'function') {
|
||||
// This call infers the type of `parentRef` as a function...
|
||||
parentRef(instance);
|
||||
} else {
|
||||
// So this assignment fails since we don't know its a ref
|
||||
parentRef.current = instance;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user