diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index a487b5086c0fe..6e6643cd1d68f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -9,15 +9,7 @@ import * as t from '@babel/types'; import {ZodError, z} from 'zod'; import {fromZodError} from 'zod-validation-error'; import {CompilerError} from '../CompilerError'; -import { - CompilationMode, - defaultOptions, - Logger, - PanicThresholdOptions, - parsePluginOptions, - PluginOptions, - ProgramContext, -} from '../Entrypoint'; +import {Logger, ProgramContext} from '../Entrypoint'; import {Err, Ok, Result} from '../Utils/Result'; import { DEFAULT_GLOBALS, @@ -158,7 +150,7 @@ export type Hook = z.infer; * missing some recursive Object / Function shapeIds */ -const EnvironmentConfigSchema = z.object({ +export const EnvironmentConfigSchema = z.object({ customHooks: z.map(z.string(), HookSchema).default(new Map()), /** @@ -640,191 +632,6 @@ const EnvironmentConfigSchema = z.object({ export type EnvironmentConfig = z.infer; -/** - * For test fixtures and playground only. - * - * Pragmas are straightforward to parse for boolean options (`:true` and - * `:false`). These are 'enabled' config values for non-boolean configs (i.e. - * what is used when parsing `:true`). - */ -const testComplexConfigDefaults: PartialEnvironmentConfig = { - validateNoCapitalizedCalls: [], - enableChangeDetectionForDebugging: { - source: 'react-compiler-runtime', - importSpecifierName: '$structuralCheck', - }, - enableEmitFreeze: { - source: 'react-compiler-runtime', - importSpecifierName: 'makeReadOnly', - }, - enableEmitInstrumentForget: { - fn: { - source: 'react-compiler-runtime', - importSpecifierName: 'useRenderCounter', - }, - gating: { - source: 'react-compiler-runtime', - importSpecifierName: 'shouldInstrument', - }, - globalGating: 'DEV', - }, - enableEmitHookGuards: { - source: 'react-compiler-runtime', - importSpecifierName: '$dispatcherGuard', - }, - inlineJsxTransform: { - elementSymbol: 'react.transitional.element', - globalDevVar: 'DEV', - }, - lowerContextAccess: { - source: 'react-compiler-runtime', - importSpecifierName: 'useContext_withSelector', - }, - inferEffectDependencies: [ - { - function: { - source: 'react', - importSpecifierName: 'useEffect', - }, - numRequiredArgs: 1, - }, - { - function: { - source: 'shared-runtime', - importSpecifierName: 'useSpecialEffect', - }, - numRequiredArgs: 2, - }, - { - function: { - source: 'useEffectWrapper', - importSpecifierName: 'default', - }, - numRequiredArgs: 1, - }, - ], -}; - -/** - * For snap test fixtures and playground only. - */ -function parseConfigPragmaEnvironmentForTest( - pragma: string, -): EnvironmentConfig { - const maybeConfig: any = {}; - // Get the defaults to programmatically check for boolean properties - const defaultConfig = EnvironmentConfigSchema.parse({}); - - for (const token of pragma.split(' ')) { - if (!token.startsWith('@')) { - continue; - } - const keyVal = token.slice(1); - let [key, val = undefined] = keyVal.split(':'); - const isSet = val === undefined || val === 'true'; - - if (isSet && key in testComplexConfigDefaults) { - maybeConfig[key] = - testComplexConfigDefaults[key as keyof PartialEnvironmentConfig]; - continue; - } - - if (key === 'customMacros' && val) { - const valSplit = val.split('.'); - if (valSplit.length > 0) { - const props = []; - for (const elt of valSplit.slice(1)) { - if (elt === '*') { - props.push({type: 'wildcard'}); - } else if (elt.length > 0) { - props.push({type: 'name', name: elt}); - } - } - maybeConfig[key] = [[valSplit[0], props]]; - } - continue; - } - - if ( - key !== 'enableResetCacheOnSourceFileChanges' && - typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean' - ) { - // skip parsing non-boolean properties - continue; - } - if (val === undefined || val === 'true') { - maybeConfig[key] = true; - } else { - maybeConfig[key] = false; - } - } - const config = EnvironmentConfigSchema.safeParse(maybeConfig); - if (config.success) { - /** - * Unless explicitly enabled, do not insert HMR handling code - * in test fixtures or playground to reduce visual noise. - */ - if (config.data.enableResetCacheOnSourceFileChanges == null) { - config.data.enableResetCacheOnSourceFileChanges = false; - } - return config.data; - } - CompilerError.invariant(false, { - reason: 'Internal error, could not parse config from pragma string', - description: `${fromZodError(config.error)}`, - loc: null, - suggestions: null, - }); -} -export function parseConfigPragmaForTests( - pragma: string, - defaults: { - compilationMode: CompilationMode; - }, -): PluginOptions { - const environment = parseConfigPragmaEnvironmentForTest(pragma); - let compilationMode: CompilationMode = defaults.compilationMode; - let panicThreshold: PanicThresholdOptions = 'all_errors'; - let noEmit: boolean = defaultOptions.noEmit; - for (const token of pragma.split(' ')) { - if (!token.startsWith('@')) { - continue; - } - switch (token) { - case '@compilationMode(annotation)': { - compilationMode = 'annotation'; - break; - } - case '@compilationMode(infer)': { - compilationMode = 'infer'; - break; - } - case '@compilationMode(all)': { - compilationMode = 'all'; - break; - } - case '@compilationMode(syntax)': { - compilationMode = 'syntax'; - break; - } - case '@panicThreshold(none)': { - panicThreshold = 'none'; - break; - } - case '@noEmit': { - noEmit = true; - break; - } - } - } - return parsePluginOptions({ - environment, - compilationMode, - panicThreshold, - noEmit, - }); -} - export type PartialEnvironmentConfig = Partial; export type ReactFunctionType = 'Component' | 'Hook' | 'Other'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts index 579c525dfb8dd..bbc9b325d4773 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts @@ -17,7 +17,6 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR'; export {computeDominatorTree, computePostDominatorTree} from './Dominator'; export { Environment, - parseConfigPragmaForTests, validateEnvironmentConfig, type EnvironmentConfig, type ExternalFunction, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts new file mode 100644 index 0000000000000..e221481724f86 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -0,0 +1,206 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {fromZodError} from 'zod-validation-error'; +import {CompilerError} from '../CompilerError'; +import { + CompilationMode, + defaultOptions, + PanicThresholdOptions, + parsePluginOptions, + PluginOptions, +} from '../Entrypoint'; +import {EnvironmentConfig} from '..'; +import { + EnvironmentConfigSchema, + PartialEnvironmentConfig, +} from '../HIR/Environment'; + +/** + * For test fixtures and playground only. + * + * Pragmas are straightforward to parse for boolean options (`:true` and + * `:false`). These are 'enabled' config values for non-boolean configs (i.e. + * what is used when parsing `:true`). + */ +const testComplexConfigDefaults: PartialEnvironmentConfig = { + validateNoCapitalizedCalls: [], + enableChangeDetectionForDebugging: { + source: 'react-compiler-runtime', + importSpecifierName: '$structuralCheck', + }, + enableEmitFreeze: { + source: 'react-compiler-runtime', + importSpecifierName: 'makeReadOnly', + }, + enableEmitInstrumentForget: { + fn: { + source: 'react-compiler-runtime', + importSpecifierName: 'useRenderCounter', + }, + gating: { + source: 'react-compiler-runtime', + importSpecifierName: 'shouldInstrument', + }, + globalGating: 'DEV', + }, + enableEmitHookGuards: { + source: 'react-compiler-runtime', + importSpecifierName: '$dispatcherGuard', + }, + inlineJsxTransform: { + elementSymbol: 'react.transitional.element', + globalDevVar: 'DEV', + }, + lowerContextAccess: { + source: 'react-compiler-runtime', + importSpecifierName: 'useContext_withSelector', + }, + inferEffectDependencies: [ + { + function: { + source: 'react', + importSpecifierName: 'useEffect', + }, + numRequiredArgs: 1, + }, + { + function: { + source: 'shared-runtime', + importSpecifierName: 'useSpecialEffect', + }, + numRequiredArgs: 2, + }, + { + function: { + source: 'useEffectWrapper', + importSpecifierName: 'default', + }, + numRequiredArgs: 1, + }, + ], +}; + +/** + * For snap test fixtures and playground only. + */ +function parseConfigPragmaEnvironmentForTest( + pragma: string, +): EnvironmentConfig { + const maybeConfig: any = {}; + // Get the defaults to programmatically check for boolean properties + const defaultConfig = EnvironmentConfigSchema.parse({}); + + for (const token of pragma.split(' ')) { + if (!token.startsWith('@')) { + continue; + } + const keyVal = token.slice(1); + let [key, val = undefined] = keyVal.split(':'); + const isSet = val === undefined || val === 'true'; + + if (isSet && key in testComplexConfigDefaults) { + maybeConfig[key] = + testComplexConfigDefaults[key as keyof PartialEnvironmentConfig]; + continue; + } + + if (key === 'customMacros' && val) { + const valSplit = val.split('.'); + if (valSplit.length > 0) { + const props = []; + for (const elt of valSplit.slice(1)) { + if (elt === '*') { + props.push({type: 'wildcard'}); + } else if (elt.length > 0) { + props.push({type: 'name', name: elt}); + } + } + maybeConfig[key] = [[valSplit[0], props]]; + } + continue; + } + + if ( + key !== 'enableResetCacheOnSourceFileChanges' && + typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean' + ) { + // skip parsing non-boolean properties + continue; + } + if (val === undefined || val === 'true') { + maybeConfig[key] = true; + } else { + maybeConfig[key] = false; + } + } + const config = EnvironmentConfigSchema.safeParse(maybeConfig); + if (config.success) { + /** + * Unless explicitly enabled, do not insert HMR handling code + * in test fixtures or playground to reduce visual noise. + */ + if (config.data.enableResetCacheOnSourceFileChanges == null) { + config.data.enableResetCacheOnSourceFileChanges = false; + } + return config.data; + } + CompilerError.invariant(false, { + reason: 'Internal error, could not parse config from pragma string', + description: `${fromZodError(config.error)}`, + loc: null, + suggestions: null, + }); +} +export function parseConfigPragmaForTests( + pragma: string, + defaults: { + compilationMode: CompilationMode; + }, +): PluginOptions { + const environment = parseConfigPragmaEnvironmentForTest(pragma); + let compilationMode: CompilationMode = defaults.compilationMode; + let panicThreshold: PanicThresholdOptions = 'all_errors'; + let noEmit: boolean = defaultOptions.noEmit; + for (const token of pragma.split(' ')) { + if (!token.startsWith('@')) { + continue; + } + switch (token) { + case '@compilationMode(annotation)': { + compilationMode = 'annotation'; + break; + } + case '@compilationMode(infer)': { + compilationMode = 'infer'; + break; + } + case '@compilationMode(all)': { + compilationMode = 'all'; + break; + } + case '@compilationMode(syntax)': { + compilationMode = 'syntax'; + break; + } + case '@panicThreshold(none)': { + panicThreshold = 'none'; + break; + } + case '@noEmit': { + noEmit = true; + break; + } + } + } + return parsePluginOptions({ + environment, + compilationMode, + panicThreshold, + noEmit, + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index 60865b8aa7bb6..086e010fea581 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -30,7 +30,6 @@ export { export { Effect, ValueKind, - parseConfigPragmaForTests, printHIR, printFunctionWithOutlined, validateEnvironmentConfig, @@ -43,6 +42,7 @@ export { printReactiveFunction, printReactiveFunctionWithOutlined, } from './ReactiveScopes'; +export {parseConfigPragmaForTests} from './Utils/TestUtils'; declare global { let __DEV__: boolean | null | undefined; } diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 6fce6445420f1..bc9578e0ee891 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -19,10 +19,10 @@ import type { CompilerPipelineValue, } from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR'; +import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; import type { Macro, MacroMethod, - parseConfigPragmaForTests as ParseConfigPragma, } from 'babel-plugin-react-compiler/src/HIR/Environment'; import * as HermesParser from 'hermes-parser'; import invariant from 'invariant'; diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index a72acf34db9bc..2478e6a545b72 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -7,7 +7,7 @@ import {codeFrameColumns} from '@babel/code-frame'; import type {PluginObj} from '@babel/core'; -import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment'; +import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; import type {printFunctionWithOutlined as PrintFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR'; import type {printReactiveFunctionWithOutlined as PrintReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction'; import {TransformResult, transformFixtureInput} from './compiler';