Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9ccd1d1b | ||
|
|
720bb13069 | ||
|
|
1eca9a2747 | ||
|
|
cd85bb5616 | ||
|
|
07e4974bad | ||
|
|
d91d28c8ba | ||
|
|
b4fe1e6c7e | ||
|
|
b204edda3a | ||
|
|
115e3ec15f | ||
|
|
565eb7888e | ||
|
|
d415fd3ed7 | ||
|
|
5e3cd53f20 | ||
|
|
01cad9eaca |
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(3).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(3).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(3).allInnerTexts()) ?? [];
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
let output: string;
|
||||
if (t.noFormat) {
|
||||
output = text.join('');
|
||||
|
||||
@@ -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, useRef, useEffect} from 'react';
|
||||
import React, {useState, useRef} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
@@ -145,6 +145,7 @@ function ExpandedEditor({
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
@@ -170,6 +171,7 @@ function ExpandedEditor({
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
|
||||
@@ -145,6 +145,7 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
className="monaco-editor-input"
|
||||
options={monacoOptions}
|
||||
loading={''}
|
||||
/>
|
||||
|
||||
@@ -340,6 +340,7 @@ function TextTabContent({
|
||||
language={language ?? 'javascript'}
|
||||
value={output}
|
||||
loading={''}
|
||||
className="monaco-editor-output"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
readOnly: true,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
|
||||
text: string,
|
||||
file: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: Partial<PluginOptions> | null,
|
||||
options: PluginOptions | null,
|
||||
includeAst: boolean = false,
|
||||
): BabelCore.BabelFileResult {
|
||||
const ast = BabelParser.parse(text, {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {LoggerEvent, ParsedPluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: PluginOptions;
|
||||
opts: ParsedPluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
@@ -66,7 +66,7 @@ export class ProgramContext {
|
||||
* Program and environment context
|
||||
*/
|
||||
scope: BabelScope;
|
||||
opts: PluginOptions;
|
||||
opts: ParsedPluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
|
||||
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
|
||||
.default(null);
|
||||
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
|
||||
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
export type PluginOptions = Partial<{
|
||||
environment: Partial<EnvironmentConfig>;
|
||||
|
||||
logger: Logger | null;
|
||||
|
||||
@@ -166,7 +166,11 @@ export type PluginOptions = {
|
||||
* a userspace approximation of runtime APIs.
|
||||
*/
|
||||
target: CompilerReactTarget;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ParsedPluginOptions = Required<
|
||||
Omit<PluginOptions, 'environment'>
|
||||
> & {environment: EnvironmentConfig};
|
||||
|
||||
const CompilerReactTargetSchema = z.union([
|
||||
z.literal('17'),
|
||||
@@ -282,7 +286,7 @@ export type Logger = {
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
};
|
||||
|
||||
export const defaultOptions: PluginOptions = {
|
||||
export const defaultOptions: ParsedPluginOptions = {
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: parseEnvironmentConfig({}).unwrap(),
|
||||
@@ -299,9 +303,9 @@ export const defaultOptions: PluginOptions = {
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
} as const;
|
||||
};
|
||||
|
||||
export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
|
||||
if (obj == null || typeof obj !== 'object') {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
const testComplexPluginOptionDefaults: PluginOptions = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
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_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
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 phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
## 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}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -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,71 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { input: t1 } = t0;
|
||||
const input = t1 === undefined ? "empty" : t1;
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[0] !== input) {
|
||||
t2 = () => {
|
||||
setCurrInput(input + "local const");
|
||||
};
|
||||
t3 = [input, "local const"];
|
||||
$[0] = input;
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== currInput) {
|
||||
t4 = <div>{currInput}</div>;
|
||||
$[3] = currInput;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ input: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>testlocal const</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,70 @@
|
||||
|
||||
## 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>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { shouldChange } = t0;
|
||||
const [count, setCount] = useState(0);
|
||||
let t1;
|
||||
if ($[0] !== count || $[1] !== shouldChange) {
|
||||
t1 = () => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
};
|
||||
$[0] = count;
|
||||
$[1] = shouldChange;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== count) {
|
||||
t2 = [count];
|
||||
$[3] = count;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== count) {
|
||||
t3 = <div>{count}</div>;
|
||||
$[5] = count;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -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,108 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { firstName } = t0;
|
||||
const [lastName, setLastName] = useState("Doe");
|
||||
const [fullName, setFullName] = useState("John");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== firstName || $[1] !== lastName) {
|
||||
t1 = () => {
|
||||
setFullName(firstName + " " + "D." + " " + lastName);
|
||||
};
|
||||
t2 = [firstName, "D.", lastName];
|
||||
$[0] = firstName;
|
||||
$[1] = lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setLastName(e.target.value);
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== lastName) {
|
||||
t4 = <input value={lastName} onChange={t3} />;
|
||||
$[5] = lastName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== fullName) {
|
||||
t5 = <div>{fullName}</div>;
|
||||
$[7] = fullName;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== t4 || $[10] !== t5) {
|
||||
t6 = (
|
||||
<div>
|
||||
{t4}
|
||||
{t5}
|
||||
</div>
|
||||
);
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>
|
||||
@@ -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,71 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -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,86 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function localFunction() {
|
||||
console.log("local function");
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const localFunction = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== propValue) {
|
||||
t2 = () => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
};
|
||||
t3 = [propValue];
|
||||
$[1] = propValue;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== value) {
|
||||
t4 = <div>{value}</div>;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['local function']
|
||||
@@ -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,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,43 @@
|
||||
|
||||
## 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'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.effect-with-global-function-call-no-error.ts:7:4
|
||||
5 | const [value, setValue] = useState(null);
|
||||
6 | useEffect(() => {
|
||||
> 7 | setValue(propValue);
|
||||
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
8 | globalCall();
|
||||
9 | }, [propValue]);
|
||||
10 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @validateNoDerivedComputationsInEffects_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,73 @@
|
||||
|
||||
## 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: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(5);
|
||||
const [firstName] = useState("Taylor");
|
||||
|
||||
const [fullName, setFullName] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== firstName) {
|
||||
t0 = () => {
|
||||
setFullName(firstName + " " + "Swift");
|
||||
};
|
||||
t1 = [firstName, "Swift"];
|
||||
$[0] = firstName;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== fullName) {
|
||||
t2 = <div>{fullName}</div>;
|
||||
$[3] = fullName;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Taylor Swift</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,72 @@
|
||||
|
||||
## 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: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</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,74 @@
|
||||
|
||||
## 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'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { props } = t0;
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + " " + props.lastName,
|
||||
);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== props.firstName || $[1] !== props.lastName) {
|
||||
t1 = () => {
|
||||
setFullName(props.firstName + " " + props.lastName);
|
||||
};
|
||||
t2 = [props.firstName, props.lastName];
|
||||
$[0] = props.firstName;
|
||||
$[1] = props.lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== fullName) {
|
||||
t3 = <div>{fullName}</div>;
|
||||
$[4] = fullName;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ props: { firstName: "John", lastName: "Doe" } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>John Doe</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'}}],
|
||||
};
|
||||
@@ -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>;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -2100,6 +2100,7 @@ export function startViewTransition(
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
blockedCallback: string => void, // Profiling-only
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
@@ -2302,6 +2303,9 @@ export function startViewTransition(
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.__reactViewTransition = null;
|
||||
}
|
||||
if (enableProfilerTimer) {
|
||||
finishedAnimation();
|
||||
}
|
||||
passiveCallback();
|
||||
});
|
||||
return transition;
|
||||
@@ -5965,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),
|
||||
};
|
||||
|
||||
@@ -5976,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
|
||||
@@ -6123,6 +6129,7 @@ export function suspendOnActiveViewTransition(
|
||||
return;
|
||||
}
|
||||
state.count++;
|
||||
state.waitingForViewTransition = true;
|
||||
const ping = onUnsuspend.bind(state);
|
||||
activeViewTransition.finished.then(ping, ping);
|
||||
}
|
||||
@@ -6206,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>,
|
||||
|
||||
@@ -674,6 +674,7 @@ export function startViewTransition(
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
blockedCallback: string => void, // Profiling-only
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
@@ -806,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,7 @@ export function logSuspendedCommitPhase(
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
'Suspended on CSS or Images',
|
||||
reason,
|
||||
startTime,
|
||||
endTime,
|
||||
currentTrack,
|
||||
@@ -1493,7 +1458,7 @@ export function logAnimatingPhase(
|
||||
endTime,
|
||||
currentTrack,
|
||||
LANES_TRACK_GROUP,
|
||||
'secondary',
|
||||
'secondary-dark',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -1503,7 +1468,7 @@ export function logAnimatingPhase(
|
||||
endTime,
|
||||
currentTrack,
|
||||
LANES_TRACK_GROUP,
|
||||
'secondary',
|
||||
'secondary-dark',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
195
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
195
packages/react-reconciler/src/ReactFiberWorkLoop.js
vendored
@@ -79,7 +79,6 @@ import {
|
||||
logErroredRenderPhase,
|
||||
logInconsistentRender,
|
||||
logSuspendedWithDelayPhase,
|
||||
logSuspenseThrottlePhase,
|
||||
logSuspendedCommitPhase,
|
||||
logSuspendedViewTransitionPhase,
|
||||
logCommitPhase,
|
||||
@@ -103,6 +102,7 @@ import {
|
||||
startSuspendingCommit,
|
||||
suspendOnActiveViewTransition,
|
||||
waitForCommitToBeReady,
|
||||
getSuspendedCommitReason,
|
||||
preloadInstance,
|
||||
preloadResource,
|
||||
supportsHydration,
|
||||
@@ -179,6 +179,8 @@ import {
|
||||
includesOnlyTransitions,
|
||||
includesBlockingLane,
|
||||
includesTransitionLane,
|
||||
includesRetryLane,
|
||||
includesIdleGroupLanes,
|
||||
includesExpiredLane,
|
||||
getNextLanes,
|
||||
getEntangledLanes,
|
||||
@@ -201,6 +203,9 @@ import {
|
||||
includesOnlyViewTransitionEligibleLanes,
|
||||
isGestureRender,
|
||||
GestureLane,
|
||||
SomeTransitionLane,
|
||||
SomeRetryLane,
|
||||
IdleLane,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
DiscreteEventPriority,
|
||||
@@ -292,6 +297,8 @@ import {
|
||||
clearTransitionTimers,
|
||||
clampBlockingTimers,
|
||||
clampTransitionTimers,
|
||||
clampRetryTimers,
|
||||
clampIdleTimers,
|
||||
markNestedUpdateScheduled,
|
||||
renderStartTime,
|
||||
commitStartTime,
|
||||
@@ -312,6 +319,12 @@ import {
|
||||
resetCommitErrors,
|
||||
PINGED_UPDATE,
|
||||
SPAWNED_UPDATE,
|
||||
startAnimating,
|
||||
stopAnimating,
|
||||
animatingLanes,
|
||||
retryClampTime,
|
||||
idleClampTime,
|
||||
animatingTask,
|
||||
} from './ReactProfilerTimer';
|
||||
|
||||
// DEV stuff
|
||||
@@ -672,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;
|
||||
@@ -703,7 +714,7 @@ 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
|
||||
|
||||
@@ -1391,7 +1402,7 @@ function finishConcurrentRender(
|
||||
workInProgressSuspendedRetryLanes,
|
||||
exitStatus,
|
||||
null,
|
||||
IMMEDIATE_COMMIT,
|
||||
null,
|
||||
renderStartTime,
|
||||
renderEndTime,
|
||||
);
|
||||
@@ -1428,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,
|
||||
@@ -1442,7 +1454,7 @@ function finishConcurrentRender(
|
||||
workInProgressSuspendedRetryLanes,
|
||||
workInProgressRootDidSkipSuspendedSiblings,
|
||||
exitStatus,
|
||||
THROTTLED_COMMIT,
|
||||
'Throttled',
|
||||
renderStartTime,
|
||||
renderEndTime,
|
||||
),
|
||||
@@ -1463,7 +1475,7 @@ function finishConcurrentRender(
|
||||
workInProgressSuspendedRetryLanes,
|
||||
workInProgressRootDidSkipSuspendedSiblings,
|
||||
exitStatus,
|
||||
IMMEDIATE_COMMIT,
|
||||
null,
|
||||
renderStartTime,
|
||||
renderEndTime,
|
||||
);
|
||||
@@ -1541,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,
|
||||
@@ -1555,7 +1568,9 @@ function commitRootWhenReady(
|
||||
suspendedRetryLanes,
|
||||
exitStatus,
|
||||
suspendedState,
|
||||
SUSPENDED_COMMIT,
|
||||
enableProfilerTimer
|
||||
? getSuspendedCommitReason(suspendedState, root.containerInfo)
|
||||
: null,
|
||||
completedRenderStartTime,
|
||||
completedRenderEndTime,
|
||||
),
|
||||
@@ -1889,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1939,6 +1960,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
|
||||
}
|
||||
finalizeRender(workInProgressRootRenderLanes, renderStartTime);
|
||||
}
|
||||
const previousUpdateTask = workInProgressUpdateTask;
|
||||
|
||||
workInProgressUpdateTask = null;
|
||||
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
|
||||
@@ -1951,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(
|
||||
@@ -1994,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,
|
||||
@@ -2022,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;
|
||||
@@ -2038,6 +2096,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
|
||||
cancelPendingCommit();
|
||||
}
|
||||
|
||||
pendingEffectsLanes = NoLanes;
|
||||
|
||||
resetWorkInProgressStack();
|
||||
workInProgressRoot = root;
|
||||
const rootWorkInProgress = createWorkInProgress(root.current, null);
|
||||
@@ -3458,7 +3518,7 @@ function commitRoot(
|
||||
recoverableErrors,
|
||||
suspendedState,
|
||||
enableProfilerTimer
|
||||
? suspendedCommitReason === IMMEDIATE_COMMIT
|
||||
? suspendedCommitReason === null
|
||||
? completedRenderEndTime
|
||||
: commitStartTime
|
||||
: 0,
|
||||
@@ -3530,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,
|
||||
);
|
||||
}
|
||||
@@ -3597,6 +3652,9 @@ function commitRoot(
|
||||
|
||||
pendingEffectsStatus = PENDING_MUTATION_PHASE;
|
||||
if (enableViewTransition && willStartViewTransition) {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
startAnimating(lanes);
|
||||
}
|
||||
pendingViewTransition = startViewTransition(
|
||||
suspendedState,
|
||||
root.containerInfo,
|
||||
@@ -3608,6 +3666,10 @@ function commitRoot(
|
||||
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.
|
||||
@@ -3633,16 +3695,63 @@ function suspendedViewTransition(reason: string): void {
|
||||
// We'll split the commit into two phases, because we're suspended in the middle.
|
||||
recordCommitEndTime();
|
||||
logCommitPhase(
|
||||
pendingSuspendedCommitReason === IMMEDIATE_COMMIT
|
||||
pendingSuspendedCommitReason === null
|
||||
? pendingEffectsRenderEndTime
|
||||
: commitStartTime,
|
||||
commitEndTime,
|
||||
commitErrors,
|
||||
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
|
||||
workInProgressUpdateTask,
|
||||
workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
|
||||
);
|
||||
pendingSuspendedViewTransitionReason = reason;
|
||||
pendingSuspendedCommitReason = SUSPENDED_COMMIT;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3720,7 +3829,7 @@ function flushLayoutEffects(): void {
|
||||
commitEndTime, // The start is the end of the first commit part.
|
||||
commitStartTime, // The end is the start of the second commit part.
|
||||
suspendedViewTransitionReason,
|
||||
workInProgressUpdateTask,
|
||||
animatingTask,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3792,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,
|
||||
@@ -3825,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;
|
||||
@@ -4327,11 +4434,7 @@ function flushPassiveEffectsImpl() {
|
||||
passiveEffectStartTime = now();
|
||||
if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
|
||||
// The animation was started, so we've been animating since that happened.
|
||||
logAnimatingPhase(
|
||||
commitEndTime,
|
||||
passiveEffectStartTime,
|
||||
workInProgressUpdateTask, // TODO: Use a ViewTransition Task
|
||||
);
|
||||
logAnimatingPhase(commitEndTime, passiveEffectStartTime, animatingTask);
|
||||
} else {
|
||||
logPaintYieldPhase(
|
||||
commitEndTime,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
includesTransitionLane,
|
||||
includesBlockingLane,
|
||||
includesSyncLane,
|
||||
NoLanes,
|
||||
} from './ReactFiberLane';
|
||||
|
||||
import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig';
|
||||
@@ -88,6 +89,12 @@ export let transitionEventType: null | string = null; // Event type of the first
|
||||
export let transitionEventIsRepeat: boolean = false;
|
||||
export let transitionSuspendedTime: number = -1.1;
|
||||
|
||||
export let retryClampTime: number = -0;
|
||||
export let idleClampTime: number = -0;
|
||||
|
||||
export let animatingLanes: Lanes = NoLanes;
|
||||
export let animatingTask: null | ConsoleTask = null; // First ViewTransition applying an Animation.
|
||||
|
||||
export let yieldReason: SuspendedReason = (0: any);
|
||||
export let yieldStartTime: number = -1.1; // The time when we yielded to the event loop
|
||||
|
||||
@@ -306,6 +313,20 @@ export function clampTransitionTimers(finalTime: number): void {
|
||||
transitionClampTime = finalTime;
|
||||
}
|
||||
|
||||
export function clampRetryTimers(finalTime: number): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
retryClampTime = finalTime;
|
||||
}
|
||||
|
||||
export function clampIdleTimers(finalTime: number): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
idleClampTime = finalTime;
|
||||
}
|
||||
|
||||
export function pushNestedEffectDurations(): number {
|
||||
if (!enableProfilerTimer || !enableProfilerCommitHooks) {
|
||||
return 0;
|
||||
@@ -578,3 +599,19 @@ export function transferActualDuration(fiber: Fiber): void {
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
export function startAnimating(lanes: Lanes): void {
|
||||
animatingLanes |= lanes;
|
||||
animatingTask = null;
|
||||
}
|
||||
|
||||
export function stopAnimating(lanes: Lanes): void {
|
||||
animatingLanes &= ~lanes;
|
||||
animatingTask = null;
|
||||
}
|
||||
|
||||
export function trackAnimatingTask(task: ConsoleTask): void {
|
||||
if (animatingTask === null) {
|
||||
animatingTask = task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
|
||||
waitForCommitToBeReady(state, timeoutOffset) {
|
||||
return null;
|
||||
},
|
||||
getSuspendedCommitReason(state, rootContainer) {
|
||||
return null;
|
||||
},
|
||||
supportsMutation: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
|
||||
export const suspendOnActiveViewTransition =
|
||||
$$$config.suspendOnActiveViewTransition;
|
||||
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
|
||||
export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
|
||||
export const NotPendingTransition = $$$config.NotPendingTransition;
|
||||
export const HostTransitionContext = $$$config.HostTransitionContext;
|
||||
export const resetFormInstance = $$$config.resetFormInstance;
|
||||
|
||||
@@ -132,7 +132,19 @@ export function resolveServerReference<T>(
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO: This needs to return async: true if it's an async module.
|
||||
if (resolvedModuleData.async) {
|
||||
// If the module is marked as async in a Client Reference, we don't actually care.
|
||||
// What matters is whether the consumer wants to unwrap it or not.
|
||||
// For Server References, it is different because the consumer is completely internal
|
||||
// to the bundler. So instead of passing it to each reference we can mark it in the
|
||||
// manifest.
|
||||
return [
|
||||
resolvedModuleData.id,
|
||||
resolvedModuleData.chunks,
|
||||
name,
|
||||
1 /* async */,
|
||||
];
|
||||
}
|
||||
return [resolvedModuleData.id, resolvedModuleData.chunks, name];
|
||||
}
|
||||
|
||||
|
||||
64
packages/react-server/src/ReactFlightServer.js
vendored
64
packages/react-server/src/ReactFlightServer.js
vendored
@@ -4702,6 +4702,70 @@ function renderDebugModel(
|
||||
element._store.validated,
|
||||
];
|
||||
}
|
||||
case REACT_LAZY_TYPE: {
|
||||
// To avoid actually initializing a lazy causing a side-effect, we make
|
||||
// some assumptions about the structure of the payload even though
|
||||
// that's not really part of the contract. In practice, this is really
|
||||
// just coming from React.lazy helper or Flight.
|
||||
const lazy: LazyComponent<any, any> = (value: any);
|
||||
const payload = lazy._payload;
|
||||
|
||||
if (payload !== null && typeof payload === 'object') {
|
||||
// React.lazy constructor
|
||||
switch (payload._status) {
|
||||
case -1 /* Uninitialized */:
|
||||
case 0 /* Pending */:
|
||||
break;
|
||||
case 1 /* Resolved */: {
|
||||
const id = outlineDebugModel(request, counter, payload._result);
|
||||
return serializeLazyID(id);
|
||||
}
|
||||
case 2 /* Rejected */: {
|
||||
// We don't log these errors since they didn't actually throw into
|
||||
// Flight.
|
||||
const digest = '';
|
||||
const id = request.nextChunkId++;
|
||||
emitErrorChunk(request, id, digest, payload._result, true, null);
|
||||
return serializeLazyID(id);
|
||||
}
|
||||
}
|
||||
|
||||
// React Flight
|
||||
switch (payload.status) {
|
||||
case 'pending':
|
||||
case 'blocked':
|
||||
case 'resolved_model':
|
||||
// The value is an uninitialized model from the Flight client.
|
||||
// It's not very useful to emit that.
|
||||
break;
|
||||
case 'resolved_module':
|
||||
// The value is client reference metadata from the Flight client.
|
||||
// It's likely for SSR, so we choose not to emit it.
|
||||
break;
|
||||
case 'fulfilled': {
|
||||
const id = outlineDebugModel(request, counter, payload.value);
|
||||
return serializeLazyID(id);
|
||||
}
|
||||
case 'rejected': {
|
||||
// We don't log these errors since they didn't actually throw into
|
||||
// Flight.
|
||||
const digest = '';
|
||||
const id = request.nextChunkId++;
|
||||
emitErrorChunk(request, id, digest, payload.reason, true, null);
|
||||
return serializeLazyID(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We couldn't emit a resolved or rejected value synchronously. For now,
|
||||
// we emit this as a halted chunk. TODO: We could maybe also handle
|
||||
// pending lazy debug models like we do in serializeDebugThenable,
|
||||
// if/when we determine that it's worth the added complexity.
|
||||
request.pendingDebugChunks++;
|
||||
const id = request.nextChunkId++;
|
||||
emitDebugHaltChunk(request, id);
|
||||
return serializeLazyID(id);
|
||||
}
|
||||
}
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
|
||||
@@ -424,6 +424,7 @@ export function startViewTransition(
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
blockedCallback: string => void, // Profiling-only
|
||||
finishedAnimation: () => void, // Profiling-only
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
@@ -589,6 +590,13 @@ export function waitForCommitToBeReady(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSuspendedCommitReason(
|
||||
state: SuspendedState,
|
||||
rootContainer: Container,
|
||||
): null | string {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const NotPendingTransition: TransitionStatus = null;
|
||||
export const HostTransitionContext: ReactContext<TransitionStatus> = {
|
||||
$$typeof: REACT_CONTEXT_TYPE,
|
||||
|
||||
@@ -37,7 +37,7 @@ The high level process of creating releases is [documented below](#process). Ind
|
||||
If your code lands in the main branch, it will be automatically published to the prerelease channels within the next weekday. However, if you want to immediately publish a prerelease, you can trigger the job to run immediately via the GitHub UI:
|
||||
|
||||
1. Wait for the commit you want to release to finish its [(Runtime) Build and Test workflow](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml), as the prerelease script needs to download the build from that workflow.
|
||||
2. Copy the git sha of whichever commit you are trying to release
|
||||
2. Copy the full git sha of whichever commit you are trying to release
|
||||
3. Go to https://github.com/facebook/react/actions/workflows/runtime_prereleases_manual.yml
|
||||
4. Paste the git sha into the "Run workflow" dropdown
|
||||
5. Let the job finish and it will be released on npm
|
||||
|
||||
Reference in New Issue
Block a user