Skip to content

Commit f21beb0

Browse files
authored
Merge branch 'main' into compiler-playground-config/3
2 parents 6f5cf5d + b9a0453 commit f21beb0

File tree

109 files changed

+2601
-620
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+2601
-620
lines changed

.codesandbox/ci.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
33
"buildCommand": "download-build-in-codesandbox-ci",
4-
"node": "18",
4+
"node": "20",
55
"publishDirectory": {
66
"react": "build/oss-experimental/react",
77
"react-dom": "build/oss-experimental/react-dom",

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ module.exports = {
577577
$AsyncIterator: 'readonly',
578578
Iterator: 'readonly',
579579
AsyncIterator: 'readonly',
580+
IntervalID: 'readonly',
580581
IteratorResult: 'readonly',
581582
JSONValue: 'readonly',
582583
JSResourceReference: 'readonly',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
/// <reference path="./.next/types/routes.d.ts" />
34

45
// NOTE: This file should not be edited
56
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

compiler/apps/playground/package.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,30 @@
3434
"invariant": "^2.2.4",
3535
"lz-string": "^1.5.0",
3636
"monaco-editor": "^0.52.0",
37-
"next": "^15.2.0-canary.64",
37+
"next": "15.5.2",
3838
"notistack": "^3.0.0-alpha.7",
3939
"prettier": "^3.3.3",
4040
"pretty-format": "^29.3.1",
4141
"re-resizable": "^6.9.16",
42-
"react": "^19.0.0",
43-
"react-dom": "^19.0.0"
42+
"react": "19.1.1",
43+
"react-dom": "19.1.1"
4444
},
4545
"devDependencies": {
4646
"@types/node": "18.11.9",
47-
"@types/react": "^19.0.0",
48-
"@types/react-dom": "^19.0.0",
47+
"@types/react": "19.1.12",
48+
"@types/react-dom": "19.1.9",
4949
"autoprefixer": "^10.4.13",
5050
"clsx": "^1.2.1",
5151
"concurrently": "^7.4.0",
5252
"eslint": "^8.28.0",
53-
"eslint-config-next": "^15.0.1",
53+
"eslint-config-next": "15.5.2",
5454
"monaco-editor-webpack-plugin": "^7.1.0",
5555
"postcss": "^8.4.31",
5656
"tailwindcss": "^3.2.4",
5757
"wait-on": "^7.2.0"
58+
},
59+
"resolutions": {
60+
"@types/react": "19.1.12",
61+
"@types/react-dom": "19.1.9"
5862
}
5963
}

compiler/apps/playground/yarn.lock

Lines changed: 932 additions & 256 deletions
Large diffs are not rendered by default.

compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export enum ErrorSeverity {
3636
* memoization.
3737
*/
3838
CannotPreserveMemoization = 'CannotPreserveMemoization',
39+
/**
40+
* An API that is known to be incompatible with the compiler. Generally as a result of
41+
* the library using "interior mutability", ie having a value whose referential identity
42+
* stays the same but which provides access to values that can change. For example a
43+
* function that doesn't change but returns different results, or an object that doesn't
44+
* change identity but whose properties change.
45+
*/
46+
IncompatibleLibrary = 'IncompatibleLibrary',
3947
/**
4048
* Unhandled syntax that we don't support yet.
4149
*/
@@ -458,7 +466,8 @@ export class CompilerError extends Error {
458466
case ErrorSeverity.InvalidJS:
459467
case ErrorSeverity.InvalidReact:
460468
case ErrorSeverity.InvalidConfig:
461-
case ErrorSeverity.UnsupportedJS: {
469+
case ErrorSeverity.UnsupportedJS:
470+
case ErrorSeverity.IncompatibleLibrary: {
462471
return true;
463472
}
464473
case ErrorSeverity.CannotPreserveMemoization:
@@ -506,8 +515,9 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
506515
severityCategory = 'Error';
507516
break;
508517
}
518+
case ErrorSeverity.IncompatibleLibrary:
509519
case ErrorSeverity.CannotPreserveMemoization: {
510-
severityCategory = 'Memoization';
520+
severityCategory = 'Compilation Skipped';
511521
break;
512522
}
513523
case ErrorSeverity.Invariant: {
@@ -547,6 +557,9 @@ export enum ErrorCategory {
547557
// Checks that manual memoization is preserved
548558
PreserveManualMemo = 'PreserveManualMemo',
549559

560+
// Checks for known incompatible libraries
561+
IncompatibleLibrary = 'IncompatibleLibrary',
562+
550563
// Checking for no mutations of props, hook arguments, hook return values
551564
Immutability = 'Immutability',
552565

@@ -870,6 +883,15 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
870883
recommended: true,
871884
};
872885
}
886+
case ErrorCategory.IncompatibleLibrary: {
887+
return {
888+
category,
889+
name: 'incompatible-library',
890+
description:
891+
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
892+
recommended: true,
893+
};
894+
}
873895
default: {
874896
assertExhaustive(category, `Unsupported category ${category}`);
875897
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {Effect, ValueKind} from '..';
9+
import {TypeConfig} from './TypeSchema';
10+
11+
/**
12+
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
13+
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
14+
*
15+
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
16+
*
17+
* ```js
18+
* function Component() {
19+
* const {someFunction} = useLibrary();
20+
* // it should always be safe to memoize functions like this
21+
* const result = useMemo(() => someFunction(), [someFunction]);
22+
* }
23+
* ```
24+
*
25+
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
26+
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
27+
* values even though the function itself is the same object. This breaks memoization, since React relies on
28+
* the outer object (or function) changing if part of its value has changed.
29+
*
30+
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
31+
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
32+
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
33+
* they may try using React Compiler, and think that the compiler has broken their code.
34+
*
35+
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
36+
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
37+
* as possible.
38+
*
39+
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
40+
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
41+
*/
42+
export function defaultModuleTypeProvider(
43+
moduleName: string,
44+
): TypeConfig | null {
45+
switch (moduleName) {
46+
case 'react-hook-form': {
47+
return {
48+
kind: 'object',
49+
properties: {
50+
useForm: {
51+
kind: 'hook',
52+
returnType: {
53+
kind: 'object',
54+
properties: {
55+
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
56+
watch: {
57+
kind: 'function',
58+
positionalParams: [],
59+
restParam: Effect.Read,
60+
calleeEffect: Effect.Read,
61+
returnType: {kind: 'type', name: 'Any'},
62+
returnValueKind: ValueKind.Mutable,
63+
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
64+
},
65+
},
66+
},
67+
},
68+
},
69+
};
70+
}
71+
case '@tanstack/react-table': {
72+
return {
73+
kind: 'object',
74+
properties: {
75+
/*
76+
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
77+
* as incompatible
78+
*/
79+
useReactTable: {
80+
kind: 'hook',
81+
positionalParams: [],
82+
restParam: Effect.Read,
83+
returnType: {kind: 'type', name: 'Any'},
84+
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
85+
},
86+
},
87+
};
88+
}
89+
}
90+
return null;
91+
}

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
import {Scope as BabelScope, NodePath} from '@babel/traverse';
5151
import {TypeSchema} from './TypeSchema';
5252
import {FlowTypeEnv} from '../Flood/Types';
53+
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
5354

5455
export const ReactElementSymbolSchema = z.object({
5556
elementSymbol: z.union([
@@ -860,10 +861,16 @@ export class Environment {
860861
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
861862
let moduleType = this.#moduleTypes.get(moduleName);
862863
if (moduleType === undefined) {
863-
if (this.config.moduleTypeProvider == null) {
864+
/*
865+
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
866+
* fallback to the default value here
867+
*/
868+
const moduleTypeProvider =
869+
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
870+
if (moduleTypeProvider == null) {
864871
return null;
865872
}
866-
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
873+
const unparsedModuleConfig = moduleTypeProvider(moduleName);
867874
if (unparsedModuleConfig != null) {
868875
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
869876
if (!parsedModuleConfig.success) {

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,7 @@ export function installTypeConfig(
10011001
mutableOnlyIfOperandsAreMutable:
10021002
typeConfig.mutableOnlyIfOperandsAreMutable === true,
10031003
aliasing: typeConfig.aliasing,
1004+
knownIncompatible: typeConfig.knownIncompatible ?? null,
10041005
});
10051006
}
10061007
case 'hook': {
@@ -1019,6 +1020,7 @@ export function installTypeConfig(
10191020
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
10201021
noAlias: typeConfig.noAlias === true,
10211022
aliasing: typeConfig.aliasing,
1023+
knownIncompatible: typeConfig.knownIncompatible ?? null,
10221024
});
10231025
}
10241026
case 'object': {

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export type FunctionSignature = {
332332
mutableOnlyIfOperandsAreMutable?: boolean;
333333

334334
impure?: boolean;
335+
knownIncompatible?: string | null | undefined;
335336

336337
canonicalName?: string;
337338

0 commit comments

Comments
 (0)