@@ -12,7 +12,7 @@ import {
12
12
CompilerErrorDetail ,
13
13
ErrorSeverity ,
14
14
} from '../CompilerError' ;
15
- import { ReactFunctionType } from '../HIR/Environment' ;
15
+ import { ExternalFunction , ReactFunctionType } from '../HIR/Environment' ;
16
16
import { CodegenFunction } from '../ReactiveScopes' ;
17
17
import { isComponentDeclaration } from '../Utils/ComponentDeclaration' ;
18
18
import { isHookDeclaration } from '../Utils/HookDeclaration' ;
@@ -31,6 +31,7 @@ import {
31
31
suppressionsToCompilerError ,
32
32
} from './Suppression' ;
33
33
import { GeneratedSource } from '../HIR' ;
34
+ import { Err , Ok , Result } from '../Utils/Result' ;
34
35
35
36
export type CompilerPass = {
36
37
opts : PluginOptions ;
@@ -40,15 +41,24 @@ export type CompilerPass = {
40
41
} ;
41
42
export const OPT_IN_DIRECTIVES = new Set ( [ 'use forget' , 'use memo' ] ) ;
42
43
export const OPT_OUT_DIRECTIVES = new Set ( [ 'use no forget' , 'use no memo' ] ) ;
44
+ const DYNAMIC_GATING_DIRECTIVE = new RegExp ( '^use memo if\\(([^\\)]*)\\)$' ) ;
43
45
44
- export function findDirectiveEnablingMemoization (
46
+ export function tryFindDirectiveEnablingMemoization (
45
47
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 ) ,
51
52
) ;
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
+ }
52
62
}
53
63
54
64
export function findDirectiveDisablingMemoization (
@@ -60,6 +70,64 @@ export function findDirectiveDisablingMemoization(
60
70
) ?? null
61
71
) ;
62
72
}
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
+ }
63
131
64
132
function isCriticalError ( err : unknown ) : boolean {
65
133
return ! ( err instanceof CompilerError ) || err . isCritical ( ) ;
@@ -477,12 +545,32 @@ function processFn(
477
545
fnType : ReactFunctionType ,
478
546
programContext : ProgramContext ,
479
547
) : null | CodegenFunction {
480
- let directives ;
548
+ let directives : {
549
+ optIn : t . Directive | null ;
550
+ optOut : t . Directive | null ;
551
+ } ;
481
552
if ( fn . node . body . type !== 'BlockStatement' ) {
482
- directives = { optIn : null , optOut : null } ;
553
+ directives = {
554
+ optIn : null ,
555
+ optOut : null ,
556
+ } ;
483
557
} 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
+ }
484
572
directives = {
485
- optIn : findDirectiveEnablingMemoization ( fn . node . body . directives ) ,
573
+ optIn : optIn . unwrapOr ( null ) ,
486
574
optOut : findDirectiveDisablingMemoization ( fn . node . body . directives ) ,
487
575
} ;
488
576
}
@@ -659,25 +747,31 @@ function applyCompiledFunctions(
659
747
pass : CompilerPass ,
660
748
programContext : ProgramContext ,
661
749
) : void {
662
- const referencedBeforeDeclared =
663
- pass . opts . gating != null
664
- ? getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns )
665
- : null ;
750
+ let referencedBeforeDeclared = null ;
666
751
for ( const result of compiledFns ) {
667
752
const { kind, originalFn, compiledFn} = result ;
668
753
const transformedFn = createNewFunctionNode ( originalFn , compiledFn ) ;
669
754
programContext . alreadyCompiled . add ( transformedFn ) ;
670
755
671
- if ( referencedBeforeDeclared != null && kind === 'original' ) {
672
- CompilerError . invariant ( pass . opts . gating != null , {
673
- reason : "Expected 'gating' import to be present" ,
674
- loc : null ,
675
- } ) ;
756
+ let dynamicGating : ExternalFunction | null = null ;
757
+ if ( originalFn . node . body . type === 'BlockStatement' ) {
758
+ const result = findDirectivesDynamicGating (
759
+ originalFn . node . body . directives ,
760
+ pass . opts ,
761
+ ) ;
762
+ if ( result . isOk ( ) ) {
763
+ dynamicGating = result . unwrap ( ) ?. gating ?? null ;
764
+ }
765
+ }
766
+ const functionGating = dynamicGating ?? pass . opts . gating ;
767
+ if ( kind === 'original' && functionGating != null ) {
768
+ referencedBeforeDeclared ??=
769
+ getFunctionReferencedBeforeDeclarationAtTopLevel ( program , compiledFns ) ;
676
770
insertGatedFunctionDeclaration (
677
771
originalFn ,
678
772
transformedFn ,
679
773
programContext ,
680
- pass . opts . gating ,
774
+ functionGating ,
681
775
referencedBeforeDeclared . has ( result ) ,
682
776
) ;
683
777
} else {
@@ -733,8 +827,13 @@ function getReactFunctionType(
733
827
) : ReactFunctionType | null {
734
828
const hookPattern = pass . opts . environment . hookPattern ;
735
829
if ( fn . node . body . type === 'BlockStatement' ) {
736
- if ( findDirectiveEnablingMemoization ( fn . node . body . directives ) != null )
830
+ const optInDirectives = tryFindDirectiveEnablingMemoization (
831
+ fn . node . body . directives ,
832
+ pass . opts ,
833
+ ) ;
834
+ if ( optInDirectives . unwrapOr ( null ) != null ) {
737
835
return getComponentOrHookLike ( fn , hookPattern ) ?? 'Other' ;
836
+ }
738
837
}
739
838
740
839
// Component and hook declarations are known components/hooks
0 commit comments