Repository: facebook/react
Pull request: [compiler][gating] Custom opt out directives (experimental option)


Adding an experimental / unstable compiler config to enable custom opt-out directives


Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
patch: @@ -41,6 +41,10 @@ const DynamicGatingOptionsSchema = z.object({
   source: z.string(),
 });
 export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
+const CustomOptOutDirectiveSchema = z
+  .nullable(z.array(z.string()))
+  .default(null);
+type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
 
 export type PluginOptions = {
   environment: EnvironmentConfig;
@@ -132,6 +136,11 @@ export type PluginOptions = {
    */
   ignoreUseNoForget: boolean;
 
+  /**
+   * Unstable / do not use
+   */
+  customOptOutDirectives: CustomOptOutDirective;
+
   sources: Array<string> | ((filename: string) => boolean) | null;
 
   /**
@@ -278,6 +287,7 @@ export const defaultOptions: PluginOptions = {
     return filename.indexOf('node_modules') === -1;
   },
   enableReanimatedCheck: true,
+  customOptOutDirectives: null,
   target: '19',
 } as const;
 
@@ -338,6 +348,21 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
           }
           break;
         }
+        case 'customOptOutDirectives': {
+          const result = CustomOptOutDirectiveSchema.safeParse(value);
+          if (result.success) {
+            parsedOptions[key] = result.data;
+          } else {
+            CompilerError.throwInvalidConfig({
+              reason:
+                'Could not parse custom opt out directives. Update React Compiler config to fix the error',
+              description: `${fromZodError(result.error)}`,
+              loc: null,
+              suggestions: null,
+            });
+          }
+          break;
+        }
         default: {
           parsedOptions[key] = value;
         }

filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
patch: @@ -63,7 +63,16 @@ export function tryFindDirectiveEnablingMemoization(
 
 export function findDirectiveDisablingMemoization(
   directives: Array<t.Directive>,
+  {customOptOutDirectives}: PluginOptions,
 ): t.Directive | null {
+  if (customOptOutDirectives != null) {
+    return (
+      directives.find(
+        directive =>
+          customOptOutDirectives.indexOf(directive.value.value) !== -1,
+      ) ?? null
+    );
+  }
   return (
     directives.find(directive =>
       OPT_OUT_DIRECTIVES.has(directive.value.value),
@@ -394,7 +403,8 @@ export function compileProgram(
     code: pass.code,
     suppressions,
     hasModuleScopeOptOut:
-      findDirectiveDisablingMemoization(program.node.directives) != null,
+      findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
+      null,
   });
 
   const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -571,7 +581,10 @@ function processFn(
     }
     directives = {
       optIn: optIn.unwrapOr(null),
-      optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
+      optOut: findDirectiveDisablingMemoization(
+        fn.node.body.directives,
+        programContext.opts,
+      ),
     };
   }
 

filename: compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
patch: @@ -93,6 +93,21 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
     },
   ],
 };
+
+function* splitPragma(
+  pragma: string,
+): Generator<{key: string; value: string | null}> {
+  for (const entry of pragma.split('@')) {
+    const keyVal = entry.trim();
+    const valIdx = keyVal.indexOf(':');
+    if (valIdx === -1) {
+      yield {key: keyVal.split(' ', 1)[0], value: null};
+    } else {
+      yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
+    }
+  }
+}
+
 /**
  * For snap test fixtures and playground only.
  */
@@ -101,19 +116,11 @@ function parseConfigPragmaEnvironmentForTest(
 ): EnvironmentConfig {
   const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
 
-  for (const token of pragma.split(' ')) {
-    if (!token.startsWith('@')) {
-      continue;
-    }
-    const keyVal = token.slice(1);
-    const valIdx = keyVal.indexOf(':');
-    const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
-    const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
-    const isSet = val === undefined || val === 'true';
+  for (const {key, value: val} of splitPragma(pragma)) {
     if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
       continue;
     }
-
+    const isSet = val == null || val === 'true';
     if (isSet && key in testComplexConfigDefaults) {
       maybeConfig[key] = testComplexConfigDefaults[key];
     } else if (isSet) {
@@ -176,18 +183,11 @@ export function parseConfigPragmaForTests(
     compilationMode: defaults.compilationMode,
     environment,
   };
-  for (const token of pragma.split(' ')) {
-    if (!token.startsWith('@')) {
-      continue;
-    }
-    const keyVal = token.slice(1);
-    const idx = keyVal.indexOf(':');
-    const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
-    const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
+  for (const {key, value: val} of splitPragma(pragma)) {
     if (!hasOwnProperty(defaultOptions, key)) {
       continue;
     }
-    const isSet = val === undefined || val === 'true';
+    const isSet = val == null || val === 'true';
     if (isSet && key in testComplexPluginOptionDefaults) {
       options[key] = testComplexPluginOptionDefaults[key];
     } else if (isSet) {

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.expect.md
patch: @@ -0,0 +1,35 @@
+
+## Input
+
+```javascript
+// @customOptOutDirectives:["use todo memo"]
+function Component() {
+  'use todo memo';
+  return <div>hello world!</div>;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Component,
+  params: [],
+};
+
+```
+
+## Code
+
+```javascript
+// @customOptOutDirectives:["use todo memo"]
+function Component() {
+  "use todo memo";
+  return <div>hello world!</div>;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Component,
+  params: [],
+};
+
+```
+      
+### Eval output
+(kind: ok) <div>hello world!</div>
\ No newline at end of file

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.tsx
patch: @@ -0,0 +1,10 @@
+// @customOptOutDirectives:["use todo memo"]
+function Component() {
+  'use todo memo';
+  return <div>hello world!</div>;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Component,
+  params: [],
+};