[compiler][entrypoint] Fix edgecases for noEmit and opt-outs (#33148)
Title --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33148). * #33149 * __->__ #33148
mofeiZ committed
May 9, 2025 at 13:37 UTC
3820740a7fbfc3b27a5127b43bdad44382ff3ce0
5 files changed
+39
-35
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
+4
@@ -59,6 +59,7 @@ type ProgramContextOptions = {
59
opts: PluginOptions;
60
filename: string | null;
61
code: string | null;
62
+ hasModuleScopeOptOut: boolean;
63
};
64
export class ProgramContext {
65
/**
@@ -70,6 +71,7 @@ export class ProgramContext {
71
code: string | null;
72
reactRuntimeModule: string;
73
suppressions: Array<SuppressionRange>;
74
+ hasModuleScopeOptOut: boolean;
75
76
/*
77
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
@@ -94,6 +96,7 @@ export class ProgramContext {
96
opts,
97
filename,
98
code,
99
+ hasModuleScopeOptOut,
100
}: ProgramContextOptions) {
101
this.scope = program.scope;
102
this.opts = opts;
@@ -101,6 +104,7 @@ export class ProgramContext {
104
this.code = code;
105
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
106
this.suppressions = suppressions;
107
+ this.hasModuleScopeOptOut = hasModuleScopeOptOut;
108
}
109
110
isHookName(name: string): boolean {
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+29
-14
@@ -325,6 +325,8 @@ export function compileProgram(
325
filename: pass.filename,
326
code: pass.code,
327
suppressions,
328
+ hasModuleScopeOptOut:
329
+ findDirectiveDisablingMemoization(program.node.directives) != null,
330
});
331
332
const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -368,7 +370,19 @@ export function compileProgram(
370
}
371
372
// Avoid modifying the program if we find a program level opt-out
371
- if (findDirectiveDisablingMemoization(program.node.directives) != null) {
373
+ if (programContext.hasModuleScopeOptOut) {
374
+ if (compiledFns.length > 0) {
375
+ const error = new CompilerError();
376
+ error.pushErrorDetail(
377
+ new CompilerErrorDetail({
378
+ reason:
379
+ 'Unexpected compiled functions when module scope opt-out is present',
380
+ severity: ErrorSeverity.Invariant,
381
+ loc: null,
382
+ }),
383
+ );
384
+ handleError(error, programContext, null);
385
+ }
386
return null;
387
}
388
@@ -491,9 +505,10 @@ function processFn(
505
}
506
507
/**
494
- * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
495
- * for validation but we don't mutate the babel AST. This allows us to flag if there is an
496
- * unused 'use no forget/memo' directive.
508
+ * If 'use no forget/memo' is present and we still ran the code through the
509
+ * compiler for validation, log a skip event and don't mutate the babel AST.
510
+ * This allows us to flag if there is an unused 'use no forget/memo'
511
+ * directive.
512
*/
513
if (
514
programContext.opts.ignoreUseNoForget === false &&
@@ -518,16 +533,7 @@ function processFn(
533
prunedMemoValues: compiledFn.prunedMemoValues,
534
});
535
521
- /**
522
- * Always compile functions with opt in directives.
523
- */
524
- if (directives.optIn != null) {
525
- return compiledFn;
526
- } else if (programContext.opts.compilationMode === 'annotation') {
527
- /**
528
- * If no opt-in directive is found and the compiler is configured in
529
- * annotation mode, don't insert the compiled function.
530
- */
536
+ if (programContext.hasModuleScopeOptOut) {
537
return null;
538
} else if (programContext.opts.noEmit) {
539
/**
@@ -541,6 +547,15 @@ function processFn(
547
}
548
}
549
return null;
550
+ } else if (
551
+ programContext.opts.compilationMode === 'annotation' &&
552
+ directives.optIn == null
553
+ ) {
554
+ /**
555
+ * If no opt-in directive is found and the compiler is configured in
556
+ * annotation mode, don't insert the compiled function.
557
+ */
558
+ return null;
559
} else {
560
return compiledFn;
561
}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md
+4
-5
@@ -33,16 +33,15 @@ export const FIXTURE_ENTRYPOINT = {
33
import { print } from "shared-runtime";
34
import useEffectWrapper from "useEffectWrapper";
35
36
-function Foo(t0) {
36
+function Foo({ propVal }) {
37
"use memo";
38
- const { propVal } = t0;
39
-
38
const arr = [propVal];
41
- useEffectWrapper(() => print(arr), [arr]);
39
+ useEffectWrapper(() => print(arr));
40
41
const arr2 = [];
44
- useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
42
+ useEffectWrapper(() => arr2.push(propVal));
43
arr2.push(2);
44
+
45
return { arr, arr2 };
46
}
47
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md
-3
@@ -20,9 +20,6 @@ function Foo() {
20
function Foo() {
21
return <button onClick={() => alert("hello!")}>Click me!</button>;
22
}
23
-function _temp() {
24
- return alert("hello!");
25
-}
23
24
```
25
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md
+2
-13
@@ -19,22 +19,11 @@ export const FIXTURE_ENTRYPOINT = {
19
## Code
20
21
```javascript
22
-import { c as _c } from "react/compiler-runtime"; // @noEmit
22
+// @noEmit
23
24
function Foo() {
25
"use memo";
26
- const $ = _c(1);
27
- let t0;
28
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29
- t0 = <button onClick={_temp}>Click me!</button>;
30
- $[0] = t0;
31
- } else {
32
- t0 = $[0];
33
- }
34
- return t0;
35
-}
36
-function _temp() {
37
- return alert("hello!");
26
+ return <button onClick={() => alert("hello!")}>Click me!</button>;
27
}
28
29
export const FIXTURE_ENTRYPOINT = {