@@ -12,7 +12,7 @@ import {
1212 CompilerErrorDetail ,
1313 ErrorSeverity ,
1414} from '../CompilerError' ;
15- import { ReactFunctionType } from '../HIR/Environment' ;
15+ import { ExternalFunction , ReactFunctionType } from '../HIR/Environment' ;
1616import { CodegenFunction } from '../ReactiveScopes' ;
1717import { isComponentDeclaration } from '../Utils/ComponentDeclaration' ;
1818import { isHookDeclaration } from '../Utils/HookDeclaration' ;
@@ -31,6 +31,7 @@ import {
3131 suppressionsToCompilerError ,
3232} from './Suppression' ;
3333import { GeneratedSource } from '../HIR' ;
34+ import { Err , Ok , Result } from '../Utils/Result' ;
3435
3536export type CompilerPass = {
3637 opts : PluginOptions ;
@@ -40,15 +41,24 @@ export type CompilerPass = {
4041} ;
4142export const OPT_IN_DIRECTIVES = new Set ( [ 'use forget' , 'use memo' ] ) ;
4243export const OPT_OUT_DIRECTIVES = new Set ( [ 'use no forget' , 'use no memo' ] ) ;
44+ const DYNAMIC_GATING_DIRECTIVE = new RegExp ( '^use memo if\\(([^\\)]*)\\)$' ) ;
4345
44- export function findDirectiveEnablingMemoization (
46+ export function tryFindDirectiveEnablingMemoization (
4547 directives : Array < t . Directive > ,
46- ) : t . Directive | null {
47- return (
48- directives . find ( directive =>
49- OPT_IN_DIRECTIVES . has ( directive . value . value ) ,
50- ) ?? null
48+ opts : PluginOptions ,
49+ ) : Result < t . Directive | null , CompilerError > {
50+ const optIn = directives . find ( directive =>
51+ OPT_IN_DIRECTIVES . has ( directive . value . value ) ,
5152 ) ;
53+ if ( optIn != null ) {
54+ return Ok ( optIn ) ;
55+ }
56+ const dynamicGating = findDirectivesDynamicGating ( directives , opts ) ;
57+ if ( dynamicGating . isOk ( ) ) {
58+ return Ok ( dynamicGating . unwrap ( ) ?. directive ?? null ) ;
59+ } else {
60+ return Err ( dynamicGating . unwrapErr ( ) ) ;
61+ }
5262}
5363
5464export function findDirectiveDisablingMemoization (
@@ -60,6 +70,64 @@ export function findDirectiveDisablingMemoization(
6070 ) ?? null
6171 ) ;
6272}
73+ function findDirectivesDynamicGating (
74+ directives : Array < t . Directive > ,
75+ opts : PluginOptions ,
76+ ) : Result <
77+ {
78+ gating : ExternalFunction ;
79+ directive : t . Directive ;
80+ } | null ,
81+ CompilerError
82+ > {
83+ if ( opts . dynamicGating === null ) {
84+ return Ok ( null ) ;
85+ }
86+ const errors = new CompilerError ( ) ;
87+ const result : Array < { directive : t . Directive ; match : string } > = [ ] ;
88+
89+ for ( const directive of directives ) {
90+ const maybeMatch = DYNAMIC_GATING_DIRECTIVE . exec ( directive . value . value ) ;
91+ if ( maybeMatch != null && maybeMatch [ 1 ] != null ) {
92+ if ( t . isValidIdentifier ( maybeMatch [ 1 ] ) ) {
93+ result . push ( { directive, match : maybeMatch [ 1 ] } ) ;
94+ } else {
95+ errors . push ( {
96+ reason : `Dynamic gating directive is not a valid JavaScript identifier` ,
97+ description : `Found '${ directive . value . value } '` ,
98+ severity : ErrorSeverity . InvalidReact ,
99+ loc : directive . loc ?? null ,
100+ suggestions : null ,
101+ } ) ;
102+ }
103+ }
104+ }
105+ if ( errors . hasErrors ( ) ) {
106+ return Err ( errors ) ;
107+ } else if ( result . length > 1 ) {
108+ const error = new CompilerError ( ) ;
109+ error . push ( {
110+ reason : `Multiple dynamic gating directives found` ,
111+ description : `Expected a single directive but found [${ result
112+ . map ( r => r . directive . value . value )
113+ . join ( ', ' ) } ]`,
114+ severity : ErrorSeverity . InvalidReact ,
115+ loc : result [ 0 ] . directive . loc ?? null ,
116+ suggestions : null ,
117+ } ) ;
118+ return Err ( error ) ;
119+ } else if ( result . length === 1 ) {
120+ return Ok ( {
121+ gating : {
122+ source : opts . dynamicGating . source ,
123+ importSpecifierName : result [ 0 ] . match ,
124+ } ,
125+ directive : result [ 0 ] . directive ,
126+ } ) ;
127+ } else {
128+ return Ok ( null ) ;
129+ }
130+ }
63131
64132function isCriticalError ( err : unknown ) : boolean {
65133 return ! ( err instanceof CompilerError ) || err . isCritical ( ) ;
@@ -477,12 +545,32 @@ function processFn(
477545 fnType : ReactFunctionType ,
478546 programContext : ProgramContext ,
479547) : null | CodegenFunction {
480- let directives ;
548+ let directives : {
549+ optIn : t . Directive | null ;
550+ optOut : t . Directive | null ;
551+ } ;
481552 if ( fn . node . body . type !== 'BlockStatement' ) {
482- directives = { optIn : null , optOut : null } ;
553+ directives = {
554+ optIn : null ,
555+ optOut : null ,
556+ } ;
483557 } else {
558+ const optIn = tryFindDirectiveEnablingMemoization (
559+ fn . node . body . directives ,
560+ programContext . opts ,
561+ ) ;
562+ if ( optIn . isErr ( ) ) {
563+ /**
564+ * If parsing opt-in directive fails, it's most likely that React Compiler
565+ * was not tested or rolled out on this function. In that case, we handle
566+ * the error and fall back to the safest option which is to not optimize
567+ * the function.
568+ */
569+ handleError ( optIn . unwrapErr ( ) , programContext , fn . node . loc ?? null ) ;
570+ return null ;
571+ }
484572 directives = {
485- optIn : findDirectiveEnablingMemoization ( fn . node . body . directives ) ,
573+ optIn : optIn . unwrapOr ( null ) ,
486574 optOut : findDirectiveDisablingMemoization ( fn . node . body . directives ) ,
487575 } ;
488576 }
@@ -661,25 +749,31 @@ function applyCompiledFunctions(
661749 pass : CompilerPass ,
662750 programContext : ProgramContext ,
663751) : void {
664- const referencedBeforeDeclared =
665- pass . opts . gating != null
666- ? getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns )
667- : null ;
752+ let referencedBeforeDeclared = null ;
668753 for ( const result of compiledFns ) {
669754 const { kind, originalFn, compiledFn} = result ;
670755 const transformedFn = createNewFunctionNode ( originalFn , compiledFn ) ;
671756 programContext . alreadyCompiled . add ( transformedFn ) ;
672757
673- if ( referencedBeforeDeclared != null && kind === 'original' ) {
674- CompilerError . invariant ( pass . opts . gating != null , {
675- reason : "Expected 'gating' import to be present" ,
676- loc : null ,
677- } ) ;
758+ let dynamicGating : ExternalFunction | null = null ;
759+ if ( originalFn . node . body . type === 'BlockStatement' ) {
760+ const result = findDirectivesDynamicGating (
761+ originalFn . node . body . directives ,
762+ pass . opts ,
763+ ) ;
764+ if ( result . isOk ( ) ) {
765+ dynamicGating = result . unwrap ( ) ?. gating ?? null ;
766+ }
767+ }
768+ const functionGating = dynamicGating ?? pass . opts . gating ;
769+ if ( kind === 'original' && functionGating != null ) {
770+ referencedBeforeDeclared ??=
771+ getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns ) ;
678772 insertGatedFunctionDeclaration (
679773 originalFn ,
680774 transformedFn ,
681775 programContext ,
682- pass . opts . gating ,
776+ functionGating ,
683777 referencedBeforeDeclared . has ( result ) ,
684778 ) ;
685779 } else {
@@ -735,8 +829,13 @@ function getReactFunctionType(
735829) : ReactFunctionType | null {
736830 const hookPattern = pass . opts . environment . hookPattern ;
737831 if ( fn . node . body . type === 'BlockStatement' ) {
738- if ( findDirectiveEnablingMemoization ( fn . node . body . directives ) != null )
832+ const optInDirectives = tryFindDirectiveEnablingMemoization (
833+ fn . node . body . directives ,
834+ pass . opts ,
835+ ) ;
836+ if ( optInDirectives . unwrapOr ( null ) != null ) {
739837 return getComponentOrHookLike ( fn , hookPattern ) ?? 'Other' ;
838+ }
740839 }
741840
742841 // Component and hook declarations are known components/hooks
0 commit comments