Merge df10e77346 into sapling-pr-archive-poteto

This commit is contained in:
lauren
2025-03-28 14:06:11 -04:00
committed by GitHub
55 changed files with 575 additions and 215 deletions
@@ -19,7 +19,9 @@ import BabelPluginReactCompiler, {
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
} from 'babel-plugin-react-compiler/src';
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
@@ -41,8 +43,6 @@ import {
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
import {transformFromAstSync} from '@babel/core';
function parseInput(
@@ -6,7 +6,7 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler/src';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
@@ -11,7 +11,7 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {type CompilerError} from 'babel-plugin-react-compiler/src';
import {type CompilerError} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
@@ -6,10 +6,7 @@
*/
import {Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler/src';
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
import {MarkerSeverity, type editor} from 'monaco-editor';
function mapReactCompilerSeverityToMonaco(
@@ -54,7 +51,7 @@ export function renderReactCompilerMarkers({
model,
details,
}: ReactCompilerMarkerConfig): void {
let markers = [];
const markers: Array<editor.IMarkerData> = [];
for (const detail of details) {
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
if (marker == null) {
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run watch\" \"yarn workspace react-compiler-runtime run watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build --dts\" \"yarn workspace react-compiler-runtime run build\"",
"build": "yarn build:compiler && next build",
"postbuild": "node ./scripts/downloadFonts.js",
"preinstall": "cd ../.. && yarn install --frozen-lockfile",
@@ -12,12 +12,12 @@
"build": "rimraf dist && tsup",
"test": "./scripts/link-react-compiler-runtime.sh && yarn snap:ci",
"jest": "yarn build && ts-node node_modules/.bin/jest",
"snap": "node ../snap/dist/main.js",
"snap": "yarn workspace snap run snap",
"snap:build": "yarn workspace snap run build",
"snap:ci": "yarn snap:build && yarn snap",
"ts:analyze-trace": "scripts/ts-analyze-trace.sh",
"lint": "yarn eslint src",
"watch": "yarn build --watch"
"watch": "yarn build --dts --watch"
},
"dependencies": {
"@babel/types": "^7.26.0"
@@ -73,7 +73,7 @@ export default function BabelPluginReactCompiler(
pass.filename ?? null,
opts.logger,
opts.environment,
result?.retryErrors ?? [],
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
@@ -30,6 +30,7 @@ import {
findProgramSuppressions,
suppressionsToCompilerError,
} from './Suppression';
import {GeneratedSource} from '../HIR';
export type CompilerPass = {
opts: PluginOptions;
@@ -267,8 +268,9 @@ function isFilePartOfSources(
return false;
}
type CompileProgramResult = {
export type CompileProgramResult = {
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
inferredEffectLocations: Set<t.SourceLocation>;
};
/**
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
@@ -369,6 +371,7 @@ export function compileProgram(
},
);
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
const inferredEffectLocations = new Set<t.SourceLocation>();
const processFn = (
fn: BabelFn,
fnType: ReactFunctionType,
@@ -509,6 +512,14 @@ export function compileProgram(
if (!pass.opts.noEmit) {
return compileResult.compiledFn;
}
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
* dependencies.
*/
for (const loc of compileResult.compiledFn.inferredEffectLocations) {
if (loc !== GeneratedSource) inferredEffectLocations.add(loc);
}
return null;
};
@@ -587,7 +598,7 @@ export function compileProgram(
if (compiledFns.length > 0) {
addImportsToProgram(program, programContext);
}
return {retryErrors};
return {retryErrors, inferredEffectLocations};
}
function shouldSkipCompilation(
@@ -11,6 +11,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramResult} from './Program';
function throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
@@ -36,12 +37,16 @@ function assertValidEffectImportReference(
const parent = path.parentPath;
if (parent != null && parent.isCallExpression()) {
const args = parent.get('arguments');
const maybeCalleeLoc = path.node.loc;
const hasInferredEffect =
maybeCalleeLoc != null &&
context.inferredEffectLocations.has(maybeCalleeLoc);
/**
* Only error on untransformed references of the form `useMyEffect(...)`
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
* TODO: do we also want a mode to also hard error on non-call references?
*/
if (args.length === numArgs) {
if (args.length === numArgs && !hasInferredEffect) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
path,
context.transformErrors,
@@ -97,7 +102,7 @@ export default function validateNoUntransformedReferences(
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
compileResult: CompileProgramResult | null,
): void {
const moduleLoadChecks = new Map<
string,
@@ -126,7 +131,7 @@ export default function validateNoUntransformedReferences(
}
}
if (moduleLoadChecks.size > 0) {
transformProgram(path, moduleLoadChecks, filename, logger, transformErrors);
transformProgram(path, moduleLoadChecks, filename, logger, compileResult);
}
}
@@ -136,6 +141,7 @@ type TraversalState = {
logger: Logger | null;
filename: string | null;
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
inferredEffectLocations: Set<t.SourceLocation>;
};
type CheckInvalidReferenceFn = (
paths: Array<NodePath<t.Node>>,
@@ -223,14 +229,16 @@ function transformProgram(
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
compileResult: CompileProgramResult | null,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,
program: path,
filename,
logger,
transformErrors,
transformErrors: compileResult?.retryErrors ?? [],
inferredEffectLocations:
compileResult?.inferredEffectLocations ?? new Set(),
};
path.traverse({
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
@@ -2406,6 +2406,19 @@ function lowerExpression(
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'cast',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
}
case 'TSSatisfiesExpression': {
let expr = exprPath as NodePath<t.TSSatisfiesExpression>;
const typeAnnotation = expr.get('typeAnnotation');
return {
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'satisfies',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
@@ -2417,6 +2430,7 @@ function lowerExpression(
kind: 'TypeCastExpression',
value: lowerExpressionToTemporary(builder, expr.get('expression')),
typeAnnotation: typeAnnotation.node,
typeAnnotationKind: 'as',
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
@@ -11,6 +11,7 @@ import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
CompilationMode,
defaultOptions,
Logger,
PanicThresholdOptions,
parsePluginOptions,
@@ -779,6 +780,7 @@ export function parseConfigPragmaForTests(
const environment = parseConfigPragmaEnvironmentForTest(pragma);
let compilationMode: CompilationMode = defaults.compilationMode;
let panicThreshold: PanicThresholdOptions = 'all_errors';
let noEmit: boolean = defaultOptions.noEmit;
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
@@ -804,12 +806,17 @@ export function parseConfigPragmaForTests(
panicThreshold = 'none';
break;
}
case '@noEmit': {
noEmit = true;
break;
}
}
}
return parsePluginOptions({
environment,
compilationMode,
panicThreshold,
noEmit,
});
}
@@ -852,6 +859,7 @@ export class Environment {
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation> = new Set();
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -910,13 +910,21 @@ export type InstructionValue =
value: Place;
loc: SourceLocation;
}
| {
| ({
kind: 'TypeCastExpression';
value: Place;
typeAnnotation: t.FlowType | t.TSType;
type: Type;
loc: SourceLocation;
}
} & (
| {
typeAnnotation: t.FlowType;
typeAnnotationKind: 'cast';
}
| {
typeAnnotation: t.TSType;
typeAnnotationKind: 'as' | 'satisfies';
}
))
| JsxExpression
| {
kind: 'ObjectExpression';
@@ -32,5 +32,5 @@ export {
} from './HIRBuilder';
export {mergeConsecutiveBlocks} from './MergeConsecutiveBlocks';
export {mergeOverlappingReactiveScopesHIR} from './MergeOverlappingReactiveScopesHIR';
export {printFunction, printHIR} from './PrintHIR';
export {printFunction, printHIR, printFunctionWithOutlined} from './PrintHIR';
export {pruneUnusedLabelsHIR} from './PruneUnusedLabelsHIR';
@@ -217,6 +217,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
// Step 2: push the inferred deps array as an argument of the useEffect
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
} else if (loadGlobals.has(value.args[0].identifier.id)) {
// Global functions have no reactive dependencies, so we can insert an empty array
newInstructions.push({
@@ -227,6 +228,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
});
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
}
}
}
@@ -104,6 +104,7 @@ export type CodegenFunction = {
* This is true if the compiler has compiled inferred effect dependencies
*/
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation>;
/**
* This is true if the compiler has compiled a fire to a useFire call
@@ -389,6 +390,7 @@ function codegenReactiveFunction(
outlined: [],
hasFireRewrite: fn.env.hasFireRewrite,
hasInferredEffect: fn.env.hasInferredEffect,
inferredEffectLocations: fn.env.inferredEffectLocations,
});
}
@@ -2113,10 +2115,17 @@ function codegenInstructionValue(
}
case 'TypeCastExpression': {
if (t.isTSType(instrValue.typeAnnotation)) {
value = t.tsAsExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
if (instrValue.typeAnnotationKind === 'satisfies') {
value = t.tsSatisfiesExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
} else {
value = t.tsAsExpression(
codegenPlaceToExpression(cx, instrValue.value),
instrValue.typeAnnotation,
);
}
} else {
value = t.typeCastExpression(
codegenPlaceToExpression(cx, instrValue.value),
@@ -14,7 +14,10 @@ export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarati
export {inferReactiveScopeVariables} from './InferReactiveScopeVariables';
export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether';
export {printReactiveFunction} from './PrintReactiveFunction';
export {
printReactiveFunction,
printReactiveFunctionWithOutlined,
} from './PrintReactiveFunction';
export {promoteUsedTemporaries} from './PromoteUsedTemporaries';
export {propagateEarlyReturns} from './PropagateEarlyReturns';
export {pruneAllReactiveScopes} from './PruneAllReactiveScopes';
@@ -0,0 +1,31 @@
## Input
```javascript
// @inferEffectDependencies @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function ReactiveVariable({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
}
```
## Code
```javascript
// @inferEffectDependencies @noEmit
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
function ReactiveVariable({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,8 @@
// @inferEffectDependencies @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
function ReactiveVariable({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr));
}
@@ -0,0 +1,71 @@
## Input
```javascript
// @enableUseTypeAnnotations
function Component(props: {id: number}) {
const x = makeArray(props.id) satisfies number[];
const y = x.at(0);
return y;
}
function makeArray<T>(x: T): Array<T> {
return [x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.id) {
t0 = makeArray(props.id);
$[0] = props.id;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0 satisfies number[];
let t1;
if ($[2] !== x) {
t1 = x.at(0);
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
const y = t1;
return y;
}
function makeArray(x) {
const $ = _c(2);
let t0;
if ($[0] !== x) {
t0 = [x];
$[0] = x;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ id: 42 }],
};
```
### Eval output
(kind: ok) 42
@@ -0,0 +1,15 @@
// @enableUseTypeAnnotations
function Component(props: {id: number}) {
const x = makeArray(props.id) satisfies number[];
const y = x.at(0);
return y;
}
function makeArray<T>(x: T): Array<T> {
return [x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
@@ -0,0 +1,41 @@
## Input
```javascript
// @enableUseTypeAnnotations
import {identity} from 'shared-runtime';
function Component(props: {id: number}) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
```
## Code
```javascript
// @enableUseTypeAnnotations
import { identity } from "shared-runtime";
function Component(props) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ id: 42 }],
};
```
### Eval output
(kind: ok) 42
@@ -0,0 +1,13 @@
// @enableUseTypeAnnotations
import {identity} from 'shared-runtime';
function Component(props: {id: number}) {
const x = identity(props.id);
const y = x satisfies number;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{id: 42}],
};
@@ -32,13 +32,17 @@ export {
ValueKind,
parseConfigPragmaForTests,
printHIR,
printFunctionWithOutlined,
validateEnvironmentConfig,
type EnvironmentConfig,
type ExternalFunction,
type Hook,
type SourceLocation,
} from './HIR';
export {printReactiveFunction} from './ReactiveScopes';
export {
printReactiveFunction,
printReactiveFunctionWithOutlined,
} from './ReactiveScopes';
declare global {
let __DEV__: boolean | null | undefined;
}
@@ -4,7 +4,7 @@
"module": "ES2015",
"moduleResolution": "Bundler",
"rootDir": "src",
"outDir": "dist",
"noEmit": true,
"jsx": "react-jsxdev",
// weaken strictness from preset
"importsNotUsedAsValues": "remove",
+2 -2
View File
@@ -11,6 +11,7 @@
"scripts": {
"postinstall": "./scripts/link-react-compiler-runtime.sh && perl -p -i -e 's/react\\.element/react.transitional.element/' ../../node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' ../../node_modules/react-dom/cjs/react-dom-test-utils.development.js",
"build": "rimraf dist && concurrently -n snap,runtime \"tsc --build\" \"yarn --silent workspace react-compiler-runtime build\"",
"snap": "node dist/main.js",
"test": "echo 'no tests'",
"prettier": "prettier --write 'src/**/*.ts'"
},
@@ -40,8 +41,7 @@
},
"devDependencies": {
"@babel/core": "^7.19.1",
"@babel/parser": "^7.19.1",
"@babel/plugin-syntax-typescript": "^7.18.6",
"@babel/parser": "^7.20.15",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"@babel/traverse": "^7.19.1",
-1
View File
@@ -187,7 +187,6 @@ function makePluginOptions(
},
logger,
gating,
noEmit: false,
eslintSuppressionRules,
flowSuppressions,
ignoreUseNoForget,
+9 -27
View File
@@ -9,35 +9,17 @@ import path from 'path';
// We assume this is run from `babel-plugin-react-compiler`
export const PROJECT_ROOT = path.normalize(
path.join(process.cwd(), '..', '..'),
path.join(process.cwd(), '..', 'babel-plugin-react-compiler'),
);
export const COMPILER_PATH = path.join(
process.cwd(),
'dist',
'Babel',
'BabelPlugin.js',
);
export const COMPILER_INDEX_PATH = path.join(process.cwd(), 'dist', 'index');
export const PRINT_HIR_PATH = path.join(
process.cwd(),
'dist',
'HIR',
'PrintHIR.js',
);
export const PRINT_REACTIVE_IR_PATH = path.join(
process.cwd(),
'dist',
'ReactiveScopes',
'PrintReactiveFunction.js',
);
export const PARSE_CONFIG_PRAGMA_PATH = path.join(
process.cwd(),
'dist',
'HIR',
'Environment.js',
export const PROJECT_SRC = path.normalize(
path.join(PROJECT_ROOT, 'dist', 'index.js'),
);
export const PRINT_HIR_IMPORT = 'printFunctionWithOutlined';
export const PRINT_REACTIVE_IR_IMPORT = 'printReactiveFunction';
export const PARSE_CONFIG_PRAGMA_IMPORT = 'parseConfigPragmaForTests';
export const FIXTURES_PATH = path.join(
process.cwd(),
PROJECT_ROOT,
'src',
'__tests__',
'fixtures',
@@ -45,4 +27,4 @@ export const FIXTURES_PATH = path.join(
);
export const SNAPSHOT_EXTENSION = '.expect.md';
export const FILTER_FILENAME = 'testfilter.txt';
export const FILTER_PATH = path.join(process.cwd(), FILTER_FILENAME);
export const FILTER_PATH = path.join(PROJECT_ROOT, FILTER_FILENAME);
+21 -11
View File
@@ -8,15 +8,16 @@
import watcher from '@parcel/watcher';
import path from 'path';
import ts from 'typescript';
import {FILTER_FILENAME, FIXTURES_PATH} from './constants';
import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, readTestFilter} from './fixture-utils';
import {execSync} from 'child_process';
export function watchSrc(
onStart: () => void,
onComplete: (isSuccess: boolean) => void,
): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
const configPath = ts.findConfigFile(
/*searchPath*/ './',
/*searchPath*/ PROJECT_ROOT,
ts.sys.fileExists,
'tsconfig.json',
);
@@ -26,10 +27,7 @@ export function watchSrc(
const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
const host = ts.createWatchCompilerHost(
configPath,
ts.convertCompilerOptionsFromJson(
{module: 'commonjs', outDir: 'dist', sourceMap: true},
'.',
).options,
undefined,
ts.sys,
createProgram,
() => {}, // we manually report errors in afterProgramCreate
@@ -41,9 +39,11 @@ export function watchSrc(
onStart();
return origCreateProgram(rootNames, options, host, oldProgram);
};
const origPostProgramCreate = host.afterProgramCreate;
host.afterProgramCreate = program => {
origPostProgramCreate!(program);
/**
* Avoid calling original postProgramCreate because it always emits tsc
* compilation output
*/
// syntactic diagnostics refer to javascript syntax
const errors = program
@@ -172,13 +172,23 @@ function subscribeTsc(
// Notify the user when compilation starts but don't clear the screen yet
console.log('\nCompiling...');
},
isSuccess => {
isTypecheckSuccess => {
let isCompilerBuildValid = false;
if (isTypecheckSuccess) {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
isCompilerBuildValid = true;
} catch (e) {
console.warn('Failed to build compiler with tsup:', e);
}
}
// Bump the compiler version after a build finishes
// and re-run tests
if (isSuccess) {
if (isCompilerBuildValid) {
state.compilerVersion++;
}
state.isCompilerBuildValid = isSuccess;
state.isCompilerBuildValid = isCompilerBuildValid;
state.mode.action = RunnerAction.Test;
onChange(state);
},
+30 -23
View File
@@ -12,16 +12,19 @@ import type {printFunctionWithOutlined as PrintFunctionWithOutlined} from 'babel
import type {printReactiveFunctionWithOutlined as PrintReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
import {TransformResult, transformFixtureInput} from './compiler';
import {
COMPILER_PATH,
COMPILER_INDEX_PATH,
PARSE_CONFIG_PRAGMA_PATH,
PRINT_HIR_PATH,
PRINT_REACTIVE_IR_PATH,
PARSE_CONFIG_PRAGMA_IMPORT,
PRINT_HIR_IMPORT,
PRINT_REACTIVE_IR_IMPORT,
PROJECT_SRC,
} from './constants';
import {TestFixture, getBasename, isExpectError} from './fixture-utils';
import {TestResult, writeOutputToString} from './reporter';
import {runSprout} from './sprout';
import {CompilerPipelineValue} from 'babel-plugin-react-compiler/src';
import type {
CompilerPipelineValue,
Effect,
ValueKind,
} from 'babel-plugin-react-compiler/src';
import chalk from 'chalk';
const originalConsoleError = console.error;
@@ -61,22 +64,29 @@ async function compile(
let compileResult: TransformResult | null = null;
let error: string | null = null;
try {
const importedCompilerPlugin = require(PROJECT_SRC) as Record<
string,
unknown
>;
// NOTE: we intentionally require lazily here so that we can clear the require cache
// and load fresh versions of the compiler when `compilerVersion` changes.
const {default: BabelPluginReactCompiler} = require(COMPILER_PATH) as {
default: PluginObj;
};
const {Effect: EffectEnum, ValueKind: ValueKindEnum} = require(
COMPILER_INDEX_PATH,
);
const {printFunctionWithOutlined} = require(PRINT_HIR_PATH) as {
printFunctionWithOutlined: typeof PrintFunctionWithOutlined;
};
const {printReactiveFunctionWithOutlined} = require(
PRINT_REACTIVE_IR_PATH,
) as {
printReactiveFunctionWithOutlined: typeof PrintReactiveFunctionWithOutlined;
};
const BabelPluginReactCompiler = importedCompilerPlugin[
'default'
] as PluginObj;
const EffectEnum = importedCompilerPlugin['Effect'] as typeof Effect;
const ValueKindEnum = importedCompilerPlugin[
'ValueKind'
] as typeof ValueKind;
const printFunctionWithOutlined = importedCompilerPlugin[
PRINT_HIR_IMPORT
] as typeof PrintFunctionWithOutlined;
const printReactiveFunctionWithOutlined = importedCompilerPlugin[
PRINT_REACTIVE_IR_IMPORT
] as typeof PrintReactiveFunctionWithOutlined;
const parseConfigPragmaForTests = importedCompilerPlugin[
PARSE_CONFIG_PRAGMA_IMPORT
] as typeof ParseConfigPragma;
let lastLogged: string | null = null;
const debugIRLogger = shouldLog
@@ -106,9 +116,6 @@ async function compile(
}
}
: () => {};
const {parseConfigPragmaForTests} = require(PARSE_CONFIG_PRAGMA_PATH) as {
parseConfigPragmaForTests: typeof ParseConfigPragma;
};
// only try logging if we filtered out all but one fixture,
// since console log order is non-deterministic
+22 -15
View File
@@ -12,7 +12,7 @@ import * as readline from 'readline';
import ts from 'typescript';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {FILTER_PATH} from './constants';
import {FILTER_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
import {TestResult, TestResults, report, update} from './reporter';
import {
@@ -22,6 +22,7 @@ import {
watchSrc,
} from './runner-watch';
import * as runnerWorker from './runner-worker';
import {execSync} from 'child_process';
const WORKER_PATH = require.resolve('./runner-worker.js');
const NUM_WORKERS = cpus().length - 1;
@@ -205,23 +206,29 @@ export async function main(opts: RunnerOptions): Promise<void> {
const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
watchSrc(
() => {},
async (compileSuccess: boolean) => {
let isSuccess = compileSuccess;
if (compileSuccess) {
const testFilter = opts.filter ? await readTestFilter() : null;
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);
} else {
const testSuccess = report(results);
isSuccess &&= testSuccess;
}
} else {
async (isTypecheckSuccess: boolean) => {
let isSuccess = false;
if (!isTypecheckSuccess) {
console.error(
'Found errors in Forget source code, skipping test fixtures.',
'Found typescript errors in Forget source code, skipping test fixtures.',
);
} else {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
const testFilter = opts.filter ? await readTestFilter() : null;
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);
isSuccess = true;
} else {
isSuccess = report(results);
}
} catch (e) {
console.warn('Failed to build compiler with tsup:', e);
}
}
tsWatch.close();
tsWatch?.close();
await worker.end();
process.exit(isSuccess ? 0 : 1);
},
+9 -2
View File
@@ -657,7 +657,7 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.1", "@babel/parser@^7.2.0":
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.2.0":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.1.tgz#6f6d6c2e621aad19a92544cc217ed13f1aac5b4c"
integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==
@@ -667,6 +667,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a"
integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==
"@babel/parser@^7.20.15":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec"
integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==
dependencies:
"@babel/types" "^7.27.0"
"@babel/parser@^7.20.7":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3"
@@ -1696,7 +1703,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.22.5", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.6", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.26.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.22.5", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.6", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.26.9", "@babel/types@^7.27.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
@@ -2,6 +2,7 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_Activity as Activity,
unstable_useSwipeTransition as useSwipeTransition,
useLayoutEffect,
useEffect,
useState,
useId,
@@ -32,7 +33,7 @@ const b = (
function Component() {
return (
<ViewTransition
className={
default={
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
}>
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
@@ -68,6 +69,16 @@ export default function Page({url, navigate}) {
return () => clearInterval(timer);
}, []);
useLayoutEffect(() => {
// Calling a default update should not interrupt ViewTransitions but
// a flushSync will.
// Promise.resolve().then(() => {
// flushSync(() => {
setCounter(c => c + 10);
// });
// });
}, [show]);
const exclamation = (
<ViewTransition name="exclamation" onShare={onTransition}>
<span>!</span>
@@ -86,17 +97,17 @@ export default function Page({url, navigate}) {
}}>
{url === '/?b' ? 'Goto A' : 'Goto B'}
</button>
<ViewTransition className="none">
<ViewTransition default="none">
<div>
<ViewTransition>
<div>
<ViewTransition className={transitions['slide-on-nav']}>
<ViewTransition default={transitions['slide-on-nav']}>
<h1>{!show ? 'A' : 'B' + counter}</h1>
</ViewTransition>
</div>
</ViewTransition>
<ViewTransition
className={{
default={{
'navigation-back': transitions['slide-right'],
'navigation-forward': transitions['slide-left'],
}}>
+6 -4
View File
@@ -538,14 +538,16 @@ export function hasInstanceAffectedParent(
}
export function startViewTransition() {
return false;
return null;
}
export type RunningGestureTransition = null;
export type RunningViewTransition = null;
export function startGestureTransition() {}
export function startGestureTransition() {
return null;
}
export function stopGestureTransition(transition: RunningGestureTransition) {}
export function stopViewTransition(transition: RunningViewTransition) {}
export type ViewTransitionInstance = null | {name: string, ...};
@@ -1687,7 +1687,7 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
): boolean {
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer: any)
@@ -1764,7 +1764,7 @@ export function startViewTransition(
}
passiveCallback();
});
return true;
return transition;
} catch (x) {
// We use the error as feature detection.
// The only thing that should throw is if startViewTransition is missing
@@ -1772,11 +1772,17 @@ export function startViewTransition(
// I.e. it's before the View Transitions v2 spec. We only support View
// Transitions v2 otherwise we fallback to not animating to ensure that
// we're not animating with the wrong animation mapped.
return false;
// Flush remaining work synchronously.
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
}
}
export type RunningGestureTransition = {
export type RunningViewTransition = {
skipTransition(): void,
...
};
@@ -1900,7 +1906,7 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
): null | RunningGestureTransition {
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer: any)
@@ -2072,13 +2078,14 @@ export function startGestureTransition(
}
}
export function stopGestureTransition(transition: RunningGestureTransition) {
export function stopViewTransition(transition: RunningViewTransition) {
transition.skipTransition();
}
interface ViewTransitionPseudoElementType extends Animatable {
_scope: HTMLElement;
_selector: string;
getComputedStyle(): CSSStyleDeclaration;
}
function ViewTransitionPseudoElement(
@@ -2132,6 +2139,14 @@ ViewTransitionPseudoElement.prototype.getAnimations = function (
}
return result;
};
// $FlowFixMe[prop-missing]
ViewTransitionPseudoElement.prototype.getComputedStyle = function (
this: ViewTransitionPseudoElementType,
): CSSStyleDeclaration {
const scope = this._scope;
const selector = this._selector;
return getComputedStyle(scope, selector);
};
export function createViewTransitionInstance(
name: string,
@@ -4664,7 +4664,7 @@ describe('ReactDOMFizzServer', () => {
// client-side rendering.
await clientResolve();
await waitForAll([
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
]);
expect(getVisibleChildren(container)).toEqual(
<div>
@@ -4712,7 +4712,7 @@ describe('ReactDOMFizzServer', () => {
},
});
await waitForAll([
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
]);
expect(getVisibleChildren(container)).toEqual(
@@ -10179,7 +10179,7 @@ describe('ReactDOMFizzServer', () => {
);
expect(recoverableErrors).toEqual([
expect.stringContaining(
"Hydration failed because the server rendered HTML didn't match the client.",
"Hydration failed because the server rendered text didn't match the client.",
),
]);
} else {
@@ -127,7 +127,7 @@ describe('ReactDOMServerHydration', () => {
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
@@ -196,7 +196,7 @@ describe('ReactDOMServerHydration', () => {
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
@@ -743,7 +743,7 @@ describe('ReactDOMServerHydration', () => {
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
"Caught [Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
@@ -3897,7 +3897,7 @@ describe('ReactDOMServerPartialHydration', () => {
});
});
assertLog([
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
]);
});
@@ -3936,7 +3936,7 @@ describe('ReactDOMServerPartialHydration', () => {
);
});
assertLog([
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
]);
});
});
@@ -320,7 +320,7 @@ describe('rendering React components at document', () => {
assertLog(
favorSafetyOverHydrationPerf
? [
"onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.",
"onRecoverableError: Hydration failed because the server rendered text didn't match the client.",
]
: [],
);
@@ -653,11 +653,16 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
): boolean {
return false;
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
}
export type RunningGestureTransition = null;
export type RunningViewTransition = null;
export function startGestureTransition(
rootContainer: Container,
@@ -668,13 +673,13 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
): RunningGestureTransition {
): null | RunningViewTransition {
mutationCallback();
animateCallback();
return null;
}
export function stopGestureTransition(transition: RunningGestureTransition) {}
export function stopViewTransition(transition: RunningViewTransition) {}
export type ViewTransitionInstance = null | {name: string, ...};
+12 -6
View File
@@ -93,7 +93,7 @@ export type TransitionStatus = mixed;
export type FormInstance = Instance;
export type RunningGestureTransition = null;
export type RunningViewTransition = null;
export type ViewTransitionInstance = null | {name: string, ...};
@@ -826,12 +826,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
afterMutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
): boolean {
return false;
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
},
startGestureTransition(
@@ -843,13 +849,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
): RunningGestureTransition {
): null | RunningViewTransition {
mutationCallback();
animateCallback();
return null;
},
stopGestureTransition(transition: RunningGestureTransition) {},
stopViewTransition(transition: RunningViewTransition) {},
createViewTransitionInstance(name: string): ViewTransitionInstance {
return null;
+6 -6
View File
@@ -151,7 +151,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
// and can stop searching (size reaches zero).
pairs.delete(name);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.share,
);
if (className !== 'none') {
@@ -196,7 +196,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
? appearingViewTransitions.get(name)
: undefined;
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
pair !== undefined ? props.share : props.enter,
);
if (className !== 'none') {
@@ -259,7 +259,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
// Note that this class name that doesn't actually really matter because the
// "new" side will be the one that wins in practice.
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.share,
);
if (className !== 'none') {
@@ -282,7 +282,7 @@ function applyExitViewTransition(placement: Fiber): void {
const props: ViewTransitionProps = placement.memoizedProps;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
// Note that just because we don't have a pair yet doesn't mean we won't find one
// later. However, that doesn't matter because if we do the class name that wins
// is the one applied by the "new" side anyway.
@@ -307,7 +307,7 @@ function applyNestedViewTransition(child: Fiber): void {
const props: ViewTransitionProps = child.memoizedProps;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.update,
);
if (className !== 'none') {
@@ -336,7 +336,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
// want the props from "current" since that's the class that would've won if
// it was the normal direction. To preserve the same effect in either direction.
const className: ?string = getViewTransitionClassName(
newProps.className,
newProps.default,
newProps.update,
);
if (className === 'none') {
+21
View File
@@ -322,6 +322,7 @@ export let didWarnAboutReassigningProps: boolean;
let didWarnAboutRevealOrder;
let didWarnAboutTailOptions;
let didWarnAboutDefaultPropsOnFunctionComponent;
let didWarnAboutClassNameOnViewTransition;
if (__DEV__) {
didWarnAboutBadClass = ({}: {[string]: boolean});
@@ -332,6 +333,7 @@ if (__DEV__) {
didWarnAboutRevealOrder = ({}: {[empty]: boolean});
didWarnAboutTailOptions = ({}: {[string]: boolean});
didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean});
didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
}
export function reconcileChildren(
@@ -3295,6 +3297,25 @@ function updateViewTransition(
pushMaterializedTreeId(workInProgress);
}
}
if (__DEV__) {
// $FlowFixMe[prop-missing]
if (pendingProps.className !== undefined) {
const example =
typeof pendingProps.className === 'string'
? JSON.stringify(pendingProps.className)
: '{...}';
if (!didWarnAboutClassNameOnViewTransition[example]) {
didWarnAboutClassNameOnViewTransition[example] = true;
console.error(
'<ViewTransition> doesn\'t accept a "className" prop. It has been renamed to "default".\n' +
'- <ViewTransition className=%s>\n' +
'+ <ViewTransition default=%s>',
example,
example,
);
}
}
}
if (current !== null && current.memoizedProps.name !== pendingProps.name) {
// If the name changes, we schedule a ref effect to create a new ref instance.
workInProgress.flags |= Ref | RefStatic;
@@ -67,6 +67,20 @@ export function trackAppearingViewTransition(
appearingViewTransitions.set(name, state);
}
export function trackEnterViewTransitions(placement: Fiber): void {
if (
placement.tag === ViewTransitionComponent ||
(placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
) {
// If an inserted or appearing Fiber is a ViewTransition component or has one as
// an immediate child, then that will trigger as an "Enter" in future passes.
// We don't do anything else for that case in the "before mutation" phase but we
// still have to mark it as needing to call startViewTransition if nothing else
// updates.
shouldStartViewTransition = true;
}
}
// We can't cancel view transition children until we know that their parent also
// don't need to transition.
export let viewTransitionCancelableChildren: null | Array<
@@ -119,7 +133,6 @@ function applyViewTransitionToHostInstancesRecursive(
let inViewport = false;
while (child !== null) {
if (child.tag === HostComponent) {
shouldStartViewTransition = true;
const instance: Instance = child.stateNode;
if (collectMeasurements !== null) {
const measurement = measureInstance(instance);
@@ -132,6 +145,7 @@ function applyViewTransitionToHostInstancesRecursive(
inViewport = true;
}
}
shouldStartViewTransition = true;
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
@@ -228,7 +242,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
}
const name = props.name;
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.share,
);
if (className !== 'none') {
@@ -267,7 +281,7 @@ export function commitEnterViewTransitions(
const props: ViewTransitionProps = placement.memoizedProps;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
state.paired ? props.share : props.enter,
);
if (className !== 'none') {
@@ -337,7 +351,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
const pair = pairs.get(name);
if (pair !== undefined) {
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.share,
);
if (className !== 'none') {
@@ -389,7 +403,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
? appearingViewTransitions.get(name)
: undefined;
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
pair !== undefined ? props.share : props.exit,
);
if (className !== 'none') {
@@ -470,7 +484,7 @@ export function commitBeforeUpdateViewTransition(
// a layout only change, then the "foo" class will be applied even though
// it was not actually an update. Which is a bug.
const className: ?string = getViewTransitionClassName(
newProps.className,
newProps.default,
newProps.update,
);
if (className === 'none') {
@@ -495,7 +509,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
const props: ViewTransitionProps = child.memoizedProps;
const name = getViewTransitionName(props, child.stateNode);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.update,
);
if (className !== 'none') {
@@ -735,7 +749,7 @@ export function measureUpdateViewTransition(
const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
// Whether it ends up having been updated or relayout we apply the update class name.
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.update,
);
if (className === 'none') {
@@ -787,7 +801,7 @@ export function measureNestedViewTransitions(
const state: ViewTransitionState = child.stateNode;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.default,
props.update,
);
let previousMeasurements: null | Array<InstanceMeasurement>;
+7
View File
@@ -235,6 +235,7 @@ import {
commitFragmentInstanceInsertionEffects,
} from './ReactFiberCommitHostEffects';
import {
trackEnterViewTransitions,
commitEnterViewTransitions,
commitExitViewTransitions,
commitBeforeUpdateViewTransition,
@@ -338,6 +339,9 @@ function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
// to trigger updates of any nested view transitions and we shouldn't
// have any other before mutation effects since snapshot effects are
// only applied to updates. TODO: Model this using only flags.
if (isViewTransitionEligible) {
trackEnterViewTransitions(fiber);
}
commitBeforeMutationEffects_complete(isViewTransitionEligible);
continue;
}
@@ -367,6 +371,9 @@ function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
// to trigger updates of any nested view transitions and we shouldn't
// have any other before mutation effects since snapshot effects are
// only applied to updates. TODO: Model this using only flags.
if (isViewTransitionEligible) {
trackEnterViewTransitions(fiber);
}
commitBeforeMutationEffects_complete(isViewTransitionEligible);
continue;
}
@@ -51,9 +51,9 @@ export const wasInstanceInViewport = shim;
export const hasInstanceChanged = shim;
export const hasInstanceAffectedParent = shim;
export const startViewTransition = shim;
export type RunningGestureTransition = null;
export type RunningViewTransition = null;
export const startGestureTransition = shim;
export const stopGestureTransition = shim;
export const stopViewTransition = shim;
export type ViewTransitionInstance = null | {name: string, ...};
export const createViewTransitionInstance = shim;
export type GestureTimeline = any;
@@ -8,10 +8,7 @@
*/
import type {FiberRoot} from './ReactInternalTypes';
import type {
GestureTimeline,
RunningGestureTransition,
} from './ReactFiberConfig';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
import {
GestureLane,
@@ -21,7 +18,7 @@ import {
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
import {
subscribeToGestureDirection,
stopGestureTransition,
stopViewTransition,
} from './ReactFiberConfig';
// This type keeps track of any scheduled or active gestures.
@@ -33,7 +30,7 @@ export type ScheduledGesture = {
rangeCurrent: number, // The starting offset along the timeline.
rangeNext: number, // The end along the timeline where the next state is reached.
cancel: () => void, // Cancel the subscription to direction change.
running: null | RunningGestureTransition, // Used to cancel the running transition after we're done.
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
};
@@ -144,7 +141,7 @@ export function cancelScheduledGesture(
} else {
gesture.running = null;
// If there's no work scheduled so we can stop the View Transition right away.
stopGestureTransition(runningTransition);
stopViewTransition(runningTransition);
}
}
}
@@ -183,7 +180,7 @@ export function stopCompletedGestures(root: FiberRoot) {
root.stoppingGestures = null;
while (gesture !== null) {
if (gesture.running !== null) {
stopGestureTransition(gesture.running);
stopViewTransition(gesture.running);
gesture.running = null;
}
const nextGesture = gesture.next;
@@ -308,7 +308,7 @@ export const HydrationMismatchException: mixed = new Error(
"userspace. If you're seeing this, it's likely a bug in React.",
);
function throwOnHydrationMismatch(fiber: Fiber) {
function throwOnHydrationMismatch(fiber: Fiber, fromText: boolean = false) {
let diff = '';
if (__DEV__) {
// Consume the diff root for this mismatch.
@@ -320,7 +320,8 @@ function throwOnHydrationMismatch(fiber: Fiber) {
}
}
const error = new Error(
"Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n" +
`Hydration failed because the server rendered ${fromText ? 'text' : 'HTML'} didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
` +
'\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
@@ -481,7 +482,7 @@ function prepareToHydrateHostInstance(
fiber,
);
if (!didHydrate && favorSafetyOverHydrationPerf) {
throwOnHydrationMismatch(fiber);
throwOnHydrationMismatch(fiber, true);
}
}
@@ -547,7 +548,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
parentProps,
);
if (!didHydrate && favorSafetyOverHydrationPerf) {
throwOnHydrationMismatch(fiber);
throwOnHydrationMismatch(fiber, true);
}
}
+6 -1
View File
@@ -310,7 +310,12 @@ function processRootScheduleInMicrotask() {
// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
// If we're in the middle of a View Transition async sequence, we don't want to
// interrupt that sequence. Instead, we'll flush any remaining work when it
// completes.
if (!hasPendingCommitEffects()) {
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}
}
function scheduleTaskForRootDuringMicrotask(
@@ -21,15 +21,19 @@ import {getIsHydrating} from './ReactFiberHydrationContext';
import {getTreeId} from './ReactFiberTreeContext';
export type ViewTransitionClassPerType = {
[transitionType: 'default' | string]: 'none' | string,
[transitionType: 'default' | string]: 'none' | 'auto' | string,
};
export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;
export type ViewTransitionClass =
| 'none'
| 'auto'
| string
| ViewTransitionClassPerType;
export type ViewTransitionProps = {
name?: string,
children?: ReactNodeList,
className?: ViewTransitionClass,
default?: ViewTransitionClass,
enter?: ViewTransitionClass,
exit?: ViewTransitionClass,
share?: ViewTransitionClass,
@@ -127,13 +131,10 @@ export function getViewTransitionClassName(
const className: ?string = getClassNameByType(defaultClass);
const eventClassName: ?string = getClassNameByType(eventClass);
if (eventClassName == null) {
return className;
return className === 'auto' ? null : className;
}
if (eventClassName === 'none') {
return eventClassName;
}
if (className != null && className !== 'none') {
return className + ' ' + eventClassName;
if (eventClassName === 'auto') {
return null;
}
return eventClassName;
}
+34 -7
View File
@@ -21,7 +21,11 @@ import type {
TransitionAbort,
} from './ReactFiberTracingMarkerComponent';
import type {OffscreenInstance} from './ReactFiberActivityComponent';
import type {Resource, ViewTransitionInstance} from './ReactFiberConfig';
import type {
Resource,
ViewTransitionInstance,
RunningViewTransition,
} from './ReactFiberConfig';
import type {RootState} from './ReactFiberRoot';
import {
getViewTransitionName,
@@ -102,6 +106,7 @@ import {
trackSchedulerEvent,
startViewTransition,
startGestureTransition,
stopViewTransition,
createViewTransitionInstance,
} from './ReactFiberConfig';
@@ -665,6 +670,7 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
let pendingPassiveTransitions: Array<Transition> | null = null;
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
let pendingViewTransition: null | RunningViewTransition = null;
let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null;
let pendingTransitionTypes: null | TransitionTypes = null;
@@ -3503,10 +3509,8 @@ function commitRoot(
}
pendingEffectsStatus = PENDING_MUTATION_PHASE;
const startedViewTransition =
enableViewTransition &&
willStartViewTransition &&
startViewTransition(
if (enableViewTransition && willStartViewTransition) {
pendingViewTransition = startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
@@ -3516,7 +3520,7 @@ function commitRoot(
flushPassiveEffects,
reportViewTransitionError,
);
if (!startedViewTransition) {
} else {
// Flush synchronously.
flushMutationEffects();
flushLayoutEffects();
@@ -3646,6 +3650,8 @@ function flushSpawnedWork(): void {
}
pendingEffectsStatus = NO_PENDING_EFFECTS;
pendingViewTransition = null; // The view transition has now fully started.
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
@@ -3915,7 +3921,7 @@ function commitGestureOnRoot(
pendingTransitionTypes = null;
pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
finishedGesture.running = startGestureTransition(
pendingViewTransition = finishedGesture.running = startGestureTransition(
root.containerInfo,
finishedGesture.provider,
finishedGesture.rangeCurrent,
@@ -3975,6 +3981,8 @@ function flushGestureAnimations(): void {
pendingFinishedWork = (null: any); // Clear for GC purposes.
pendingEffectsLanes = NoLanes;
pendingViewTransition = null; // The view transition has now fully started.
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
@@ -4025,8 +4033,27 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
}
}
let didWarnAboutInterruptedViewTransitions = false;
export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
// Returns whether passive effects were flushed.
if (enableViewTransition && pendingViewTransition !== null) {
// If we forced a flush before the View Transition full started then we skip it.
// This ensures that we're not running a partial animation.
stopViewTransition(pendingViewTransition);
if (__DEV__) {
if (!didWarnAboutInterruptedViewTransitions) {
didWarnAboutInterruptedViewTransitions = true;
console.warn(
'A flushSync update cancelled a View Transition because it was called ' +
'while the View Transition was still preparing. To preserve the synchronous ' +
'semantics, React had to skip the View Transition. If you can, try to avoid ' +
"flushSync() in a scenario that's likely to interfere.",
);
}
}
pendingViewTransition = null;
}
flushGestureMutations();
flushGestureAnimations();
flushMutationEffects();
@@ -40,7 +40,7 @@ export opaque type NoTimeout = mixed;
export opaque type RendererInspectionConfig = mixed;
export opaque type TransitionStatus = mixed;
export opaque type FormInstance = mixed;
export type RunningGestureTransition = mixed;
export type RunningViewTransition = mixed;
export type ViewTransitionInstance = null | {name: string, ...};
export opaque type InstanceMeasurement = mixed;
export type EventResponder = any;
@@ -155,7 +155,7 @@ export const hasInstanceChanged = $$$config.hasInstanceChanged;
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
export const startViewTransition = $$$config.startViewTransition;
export const startGestureTransition = $$$config.startGestureTransition;
export const stopGestureTransition = $$$config.stopGestureTransition;
export const stopViewTransition = $$$config.stopViewTransition;
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
export const subscribeToGestureDirection =
$$$config.subscribeToGestureDirection;
@@ -422,11 +422,16 @@ export function startViewTransition(
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
): boolean {
return false;
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
}
export type RunningGestureTransition = null;
export type RunningViewTransition = null;
export function startGestureTransition(
rootContainer: Container,
@@ -437,13 +442,13 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
): RunningGestureTransition {
): null | RunningViewTransition {
mutationCallback();
animateCallback();
return null;
}
export function stopGestureTransition(transition: RunningGestureTransition) {}
export function stopViewTransition(transition: RunningViewTransition) {}
export type ViewTransitionInstance = null | {name: string, ...};
+1 -1
View File
@@ -403,7 +403,7 @@
"415": "Error parsing the data. It's probably an error code or network corruption.",
"416": "This environment don't support binary chunks.",
"417": "React currently only supports piping to one writable stream.",
"418": "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s",
"418": "Hydration failed because the server rendered %s didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s",
"419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.",
"420": "ServerContext: %s already defined",
"421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.",
@@ -56,6 +56,7 @@ module.exports = () => {
const params = commandLineArgs(paramDefinitions);
splitCommaParams(params.skipPackages);
splitCommaParams(params.onlyPackages);
return params;
};
@@ -117,16 +117,7 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => {
// A separate "React version" is used for the embedded renderer version to support DevTools,
// since it needs to distinguish between different version ranges of React.
// We need to replace it as well as the "next" version number.
const buildInfoPath = join(nodeModulesPath, 'react', 'build-info.json');
const {reactVersion} = await readJson(buildInfoPath);
if (!reactVersion) {
console.error(
theme`{error Unsupported or invalid build metadata in} {path build/node_modules/react/build-info.json}` +
theme`{error . This could indicate that you have specified an outdated "next" version.}`
);
process.exit(1);
}
const reactVersion = version;
// We print the diff to the console for review,
// but it can be large so let's also write it to disk.
@@ -152,10 +143,6 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => {
while (afterContents.indexOf(version) >= 0) {
afterContents = afterContents.replace(version, newStableVersion);
}
// Replace inline renderer version numbers (e.g. shared/ReactVersion).
while (afterContents.indexOf(reactVersion) >= 0) {
afterContents = afterContents.replace(reactVersion, newStableVersion);
}
if (beforeContents !== afterContents) {
numFilesModified++;
// Using a relative path for diff helps with the snapshot test