Skip to content

Commit 50e7ec8

Browse files
authored
[compiler] Deprecate noEmit, add outputMode (facebook#35112)
This deprecates the `noEmit: boolean` flag and adds `outputMode: 'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement. OutputMode defaults to null and takes precedence if specified, otherwise we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true. Key points: * Retrying failed compilation switches from 'client' mode to 'client-no-memo' * Validations are enabled behind Environment.proto.shouldEnableValidations, enabled for all modes except 'client-no-memo'. Similar for dropping manual memoization. * OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag * Creation of reactive scopes, and related codegen logic, is now gated by outputMode==='client'
1 parent 4cf770d commit 50e7ec8

30 files changed

+514
-216
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,25 @@ export type PluginOptions = Partial<{
102102

103103
panicThreshold: PanicThresholdOptions;
104104

105-
/*
105+
/**
106+
* @deprecated
107+
*
106108
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
107109
* passes.
108110
*
111+
* NOTE: ignored if `outputMode` is specified
112+
*
109113
* Defaults to false
110114
*/
111115
noEmit: boolean;
112116

117+
/**
118+
* If specified, overrides `noEmit` and controls the output mode of the compiler.
119+
*
120+
* Defaults to null
121+
*/
122+
outputMode: CompilerOutputMode | null;
123+
113124
/*
114125
* Determines the strategy for determining which functions to compile. Note that regardless of
115126
* which mode is enabled, a component can be opted out by adding the string literal
@@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([
212223

213224
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
214225

226+
const CompilerOutputModeSchema = z.enum([
227+
// Build optimized for SSR, with client features removed
228+
'ssr',
229+
// Build optimized for the client, with auto memoization
230+
'client',
231+
// Build optimized for the client without auto memo
232+
'client-no-memo',
233+
// Lint mode, the output is unused but validations should run
234+
'lint',
235+
]);
236+
237+
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
238+
215239
/**
216240
* Represents 'events' that may occur during compilation. Events are only
217241
* recorded when a logger is set (through the config).
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
293317
logger: null,
294318
gating: null,
295319
noEmit: false,
320+
outputMode: null,
296321
dynamicGating: null,
297322
eslintSuppressionRules: null,
298323
flowSuppressions: true,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import {NodePath} from '@babel/traverse';
99
import * as t from '@babel/types';
1010
import prettyFormat from 'pretty-format';
11-
import {Logger, ProgramContext} from '.';
11+
import {CompilerOutputMode, Logger, ProgramContext} from '.';
1212
import {
1313
HIRFunction,
1414
ReactiveFunction,
@@ -24,7 +24,6 @@ import {
2424
pruneUnusedLabelsHIR,
2525
} from '../HIR';
2626
import {
27-
CompilerMode,
2827
Environment,
2928
EnvironmentConfig,
3029
ReactFunctionType,
@@ -120,7 +119,7 @@ function run(
120119
>,
121120
config: EnvironmentConfig,
122121
fnType: ReactFunctionType,
123-
mode: CompilerMode,
122+
mode: CompilerOutputMode,
124123
programContext: ProgramContext,
125124
logger: Logger | null,
126125
filename: string | null,
@@ -170,7 +169,7 @@ function runWithEnvironment(
170169
validateUseMemo(hir).unwrap();
171170

172171
if (
173-
env.isInferredMemoEnabled &&
172+
env.enableDropManualMemoization &&
174173
!env.config.enablePreserveExistingManualUseMemo &&
175174
!env.config.disableMemoizationForDebugging &&
176175
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +205,7 @@ function runWithEnvironment(
206205
inferTypes(hir);
207206
log({kind: 'hir', name: 'InferTypes', value: hir});
208207

209-
if (env.isInferredMemoEnabled) {
208+
if (env.enableValidations) {
210209
if (env.config.validateHooksUsage) {
211210
validateHooksUsage(hir).unwrap();
212211
}
@@ -232,13 +231,13 @@ function runWithEnvironment(
232231

233232
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
234233
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
235-
if (env.isInferredMemoEnabled) {
234+
if (env.enableValidations) {
236235
if (mutabilityAliasingErrors.isErr()) {
237236
throw mutabilityAliasingErrors.unwrapErr();
238237
}
239238
}
240239

241-
if (env.config.enableOptimizeForSSR) {
240+
if (env.outputMode === 'ssr') {
242241
optimizeForSSR(hir);
243242
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
244243
}
@@ -259,14 +258,14 @@ function runWithEnvironment(
259258
isFunctionExpression: false,
260259
});
261260
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
262-
if (env.isInferredMemoEnabled) {
261+
if (env.enableValidations) {
263262
if (mutabilityAliasingRangeErrors.isErr()) {
264263
throw mutabilityAliasingRangeErrors.unwrapErr();
265264
}
266265
validateLocalsNotReassignedAfterRender(hir);
267266
}
268267

269-
if (env.isInferredMemoEnabled) {
268+
if (env.enableValidations) {
270269
if (env.config.assertValidMutableRanges) {
271270
assertValidMutableRanges(hir);
272271
}
@@ -310,20 +309,18 @@ function runWithEnvironment(
310309
value: hir,
311310
});
312311

313-
if (env.isInferredMemoEnabled) {
314-
if (env.config.validateStaticComponents) {
315-
env.logErrors(validateStaticComponents(hir));
316-
}
312+
if (env.enableValidations && env.config.validateStaticComponents) {
313+
env.logErrors(validateStaticComponents(hir));
314+
}
317315

316+
if (env.enableMemoization) {
318317
/**
319318
* Only create reactive scopes (which directly map to generated memo blocks)
320319
* if inferred memoization is enabled. This makes all later passes which
321320
* transform reactive-scope labeled instructions no-ops.
322321
*/
323-
if (!env.config.enableOptimizeForSSR) {
324-
inferReactiveScopeVariables(hir);
325-
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
326-
}
322+
inferReactiveScopeVariables(hir);
323+
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
327324
}
328325

329326
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
@@ -588,7 +585,7 @@ export function compileFn(
588585
>,
589586
config: EnvironmentConfig,
590587
fnType: ReactFunctionType,
591-
mode: CompilerMode,
588+
mode: CompilerOutputMode,
592589
programContext: ProgramContext,
593590
logger: Logger | null,
594591
filename: string | null,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
validateRestrictedImports,
2525
} from './Imports';
2626
import {
27+
CompilerOutputMode,
2728
CompilerReactTarget,
2829
ParsedPluginOptions,
2930
PluginOptions,
@@ -421,9 +422,17 @@ export function compileProgram(
421422
);
422423
const compiledFns: Array<CompileResult> = [];
423424

425+
// outputMode takes precedence if specified
426+
const outputMode: CompilerOutputMode =
427+
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
424428
while (queue.length !== 0) {
425429
const current = queue.shift()!;
426-
const compiled = processFn(current.fn, current.fnType, programContext);
430+
const compiled = processFn(
431+
current.fn,
432+
current.fnType,
433+
programContext,
434+
outputMode,
435+
);
427436

428437
if (compiled != null) {
429438
for (const outlined of compiled.outlined) {
@@ -581,6 +590,7 @@ function processFn(
581590
fn: BabelFn,
582591
fnType: ReactFunctionType,
583592
programContext: ProgramContext,
593+
outputMode: CompilerOutputMode,
584594
): null | CodegenFunction {
585595
let directives: {
586596
optIn: t.Directive | null;
@@ -616,18 +626,27 @@ function processFn(
616626
}
617627

618628
let compiledFn: CodegenFunction;
619-
const compileResult = tryCompileFunction(fn, fnType, programContext);
629+
const compileResult = tryCompileFunction(
630+
fn,
631+
fnType,
632+
programContext,
633+
outputMode,
634+
);
620635
if (compileResult.kind === 'error') {
621636
if (directives.optOut != null) {
622637
logError(compileResult.error, programContext, fn.node.loc ?? null);
623638
} else {
624639
handleError(compileResult.error, programContext, fn.node.loc ?? null);
625640
}
626-
const retryResult = retryCompileFunction(fn, fnType, programContext);
627-
if (retryResult == null) {
641+
if (outputMode === 'client') {
642+
const retryResult = retryCompileFunction(fn, fnType, programContext);
643+
if (retryResult == null) {
644+
return null;
645+
}
646+
compiledFn = retryResult;
647+
} else {
628648
return null;
629649
}
630-
compiledFn = retryResult;
631650
} else {
632651
compiledFn = compileResult.compiledFn;
633652
}
@@ -663,7 +682,7 @@ function processFn(
663682

664683
if (programContext.hasModuleScopeOptOut) {
665684
return null;
666-
} else if (programContext.opts.noEmit) {
685+
} else if (programContext.opts.outputMode === 'lint') {
667686
/**
668687
* inferEffectDependencies + noEmit is currently only used for linting. In
669688
* this mode, add source locations for where the compiler *can* infer effect
@@ -693,6 +712,7 @@ function tryCompileFunction(
693712
fn: BabelFn,
694713
fnType: ReactFunctionType,
695714
programContext: ProgramContext,
715+
outputMode: CompilerOutputMode,
696716
):
697717
| {kind: 'compile'; compiledFn: CodegenFunction}
698718
| {kind: 'error'; error: unknown} {
@@ -719,7 +739,7 @@ function tryCompileFunction(
719739
fn,
720740
programContext.opts.environment,
721741
fnType,
722-
'all_features',
742+
outputMode,
723743
programContext,
724744
programContext.opts.logger,
725745
programContext.filename,
@@ -757,7 +777,7 @@ function retryCompileFunction(
757777
fn,
758778
environment,
759779
fnType,
760-
'no_inferred_memo',
780+
'client-no-memo',
761781
programContext,
762782
programContext.opts.logger,
763783
programContext.filename,

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as t from '@babel/types';
99
import {ZodError, z} from 'zod/v4';
1010
import {fromZodError} from 'zod-validation-error/v4';
1111
import {CompilerError} from '../CompilerError';
12-
import {Logger, ProgramContext} from '../Entrypoint';
12+
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
1313
import {Err, Ok, Result} from '../Utils/Result';
1414
import {
1515
DEFAULT_GLOBALS,
@@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
5151
import {TypeSchema} from './TypeSchema';
5252
import {FlowTypeEnv} from '../Flood/Types';
5353
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
54+
import {assertExhaustive} from '../Utils/utils';
5455

5556
export const ReactElementSymbolSchema = z.object({
5657
elementSymbol: z.union([
@@ -691,8 +692,6 @@ export const EnvironmentConfigSchema = z.object({
691692
* by React to only execute in response to events, not during render.
692693
*/
693694
enableInferEventHandlers: z.boolean().default(false),
694-
695-
enableOptimizeForSSR: z.boolean().default(false),
696695
});
697696

698697
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -732,7 +731,7 @@ export class Environment {
732731
code: string | null;
733732
config: EnvironmentConfig;
734733
fnType: ReactFunctionType;
735-
compilerMode: CompilerMode;
734+
outputMode: CompilerOutputMode;
736735
programContext: ProgramContext;
737736
hasFireRewrite: boolean;
738737
hasInferredEffect: boolean;
@@ -747,7 +746,7 @@ export class Environment {
747746
constructor(
748747
scope: BabelScope,
749748
fnType: ReactFunctionType,
750-
compilerMode: CompilerMode,
749+
outputMode: CompilerOutputMode,
751750
config: EnvironmentConfig,
752751
contextIdentifiers: Set<t.Identifier>,
753752
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -758,7 +757,7 @@ export class Environment {
758757
) {
759758
this.#scope = scope;
760759
this.fnType = fnType;
761-
this.compilerMode = compilerMode;
760+
this.outputMode = outputMode;
762761
this.config = config;
763762
this.filename = filename;
764763
this.code = code;
@@ -854,8 +853,65 @@ export class Environment {
854853
return this.#flowTypeEnvironment;
855854
}
856855

857-
get isInferredMemoEnabled(): boolean {
858-
return this.compilerMode !== 'no_inferred_memo';
856+
get enableDropManualMemoization(): boolean {
857+
switch (this.outputMode) {
858+
case 'lint': {
859+
// linting drops to be more compatible with compiler analysis
860+
return true;
861+
}
862+
case 'client':
863+
case 'ssr': {
864+
return true;
865+
}
866+
case 'client-no-memo': {
867+
return false;
868+
}
869+
default: {
870+
assertExhaustive(
871+
this.outputMode,
872+
`Unexpected output mode '${this.outputMode}'`,
873+
);
874+
}
875+
}
876+
}
877+
878+
get enableMemoization(): boolean {
879+
switch (this.outputMode) {
880+
case 'client':
881+
case 'lint': {
882+
// linting also enables memoization so that we can check if manual memoization is preserved
883+
return true;
884+
}
885+
case 'ssr':
886+
case 'client-no-memo': {
887+
return false;
888+
}
889+
default: {
890+
assertExhaustive(
891+
this.outputMode,
892+
`Unexpected output mode '${this.outputMode}'`,
893+
);
894+
}
895+
}
896+
}
897+
898+
get enableValidations(): boolean {
899+
switch (this.outputMode) {
900+
case 'client':
901+
case 'lint':
902+
case 'ssr': {
903+
return true;
904+
}
905+
case 'client-no-memo': {
906+
return false;
907+
}
908+
default: {
909+
assertExhaustive(
910+
this.outputMode,
911+
`Unexpected output mode '${this.outputMode}'`,
912+
);
913+
}
914+
}
859915
}
860916

861917
get nextIdentifierId(): IdentifierId {

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2452,7 +2452,7 @@ function computeEffectsForLegacySignature(
24522452
}),
24532453
});
24542454
}
2455-
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
2455+
if (signature.knownIncompatible != null && state.env.enableValidations) {
24562456
const errors = new CompilerError();
24572457
errors.pushDiagnostic(
24582458
CompilerDiagnostic.create({

0 commit comments

Comments
 (0)