@@ -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 }
@@ -658,25 +746,31 @@ function applyCompiledFunctions(
658746 pass : CompilerPass ,
659747 programContext : ProgramContext ,
660748) : void {
661- const referencedBeforeDeclared =
662- pass . opts . gating != null
663- ? getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns )
664- : null ;
749+ let referencedBeforeDeclared = null ;
665750 for ( const result of compiledFns ) {
666751 const { kind, originalFn, compiledFn} = result ;
667752 const transformedFn = createNewFunctionNode ( originalFn , compiledFn ) ;
668753 programContext . alreadyCompiled . add ( transformedFn ) ;
669754
670- if ( referencedBeforeDeclared != null && kind === 'original' ) {
671- CompilerError . invariant ( pass . opts . gating != null , {
672- reason : "Expected 'gating' import to be present" ,
673- loc : null ,
674- } ) ;
755+ let dynamicGating : ExternalFunction | null = null ;
756+ if ( originalFn . node . body . type === 'BlockStatement' ) {
757+ const result = findDirectivesDynamicGating (
758+ originalFn . node . body . directives ,
759+ pass . opts ,
760+ ) ;
761+ if ( result . isOk ( ) ) {
762+ dynamicGating = result . unwrap ( ) ?. gating ?? null ;
763+ }
764+ }
765+ const functionGating = dynamicGating ?? pass . opts . gating ;
766+ if ( kind === 'original' && functionGating != null ) {
767+ referencedBeforeDeclared ??=
768+ getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns ) ;
675769 insertGatedFunctionDeclaration (
676770 originalFn ,
677771 transformedFn ,
678772 programContext ,
679- pass . opts . gating ,
773+ functionGating ,
680774 referencedBeforeDeclared . has ( result ) ,
681775 ) ;
682776 } else {
@@ -732,8 +826,13 @@ function getReactFunctionType(
732826) : ReactFunctionType | null {
733827 const hookPattern = pass . opts . environment . hookPattern ;
734828 if ( fn . node . body . type === 'BlockStatement' ) {
735- if ( findDirectiveEnablingMemoization ( fn . node . body . directives ) != null )
829+ const optInDirectives = tryFindDirectiveEnablingMemoization (
830+ fn . node . body . directives ,
831+ pass . opts ,
832+ ) ;
833+ if ( optInDirectives . unwrapOr ( null ) != null ) {
736834 return getComponentOrHookLike ( fn , hookPattern ) ?? 'Other' ;
835+ }
737836 }
738837
739838 // Component and hook declarations are known components/hooks
0 commit comments