@samitouri / QOS-React / commits / 3820740a7f

[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 = {