From 3cbb0559b77225712ce8757098dfe4a6ec5e5be2 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Thu, 28 Aug 2025 16:48:33 -0400 Subject: [PATCH 1/4] Remove complex config defaults logic & top-level exports --- .../components/Editor/ConfigEditor.tsx | 2 +- .../src/Utils/TestUtils.ts | 19 ++----------------- .../babel-plugin-react-compiler/src/index.ts | 5 +---- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index d8b99c1dea4e4..af03167854465 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -6,7 +6,7 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler'; +import {parseConfigPragmaAsString} from '../../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; import parserBabel from 'prettier/plugins/babel'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts index 29e1da699ec33..172270a6d0c79 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -269,22 +269,9 @@ function parseConfigStringAsJS( ...parsedConfig.environment, }; - // Apply complex defaults for environment flags that are set to true - const environmentConfig: Partial> = - {}; - for (const [key, value] of Object.entries(mergedEnvironment)) { - if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) { - if (value === true && key in testComplexConfigDefaults) { - environmentConfig[key] = testComplexConfigDefaults[key]; - } else { - environmentConfig[key] = value; - } - } - } - // Validate environment config const validatedEnvironment = - EnvironmentConfigSchema.safeParse(environmentConfig); + EnvironmentConfigSchema.safeParse(mergedEnvironment); if (!validatedEnvironment.success) { CompilerError.invariant(false, { reason: 'Invalid environment configuration in config pragma', @@ -308,9 +295,7 @@ function parseConfigStringAsJS( } if (hasOwnProperty(defaultOptions, key)) { - if (value === true && key in testComplexPluginOptionDefaults) { - options[key] = testComplexPluginOptionDefaults[key]; - } else if (key === 'target' && value === 'donotuse_meta_internal') { + if (key === 'target' && value === 'donotuse_meta_internal') { options[key] = { kind: value, runtimeModule: 'react', diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index d8e3bf21def76..2830d70d95c8d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -48,10 +48,7 @@ export { printReactiveFunction, printReactiveFunctionWithOutlined, } from './ReactiveScopes'; -export { - parseConfigPragmaForTests, - parseConfigPragmaAsString, -} from './Utils/TestUtils'; +export {parseConfigPragmaForTests} from './Utils/TestUtils'; declare global { let __DEV__: boolean | null | undefined; } From 2bb805a92da54f8954a7307b7a444c7b66838718 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 29 Aug 2025 14:59:27 -0400 Subject: [PATCH 2/4] Refactor code to use less renders --- .../components/Editor/ConfigEditor.tsx | 32 ++----------------- .../components/Editor/EditorImpl.tsx | 16 +++++++--- .../playground/components/Editor/Input.tsx | 31 +++++++++++++++++- .../playground/components/StoreContext.tsx | 4 ++- compiler/apps/playground/lib/defaultStore.ts | 2 ++ compiler/apps/playground/lib/stores/store.ts | 10 ++++++ 6 files changed, 59 insertions(+), 36 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index af03167854465..31c5b09e281bd 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -6,13 +6,9 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {parseConfigPragmaAsString} from '../../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; -import parserBabel from 'prettier/plugins/babel'; -import * as prettierPluginEstree from 'prettier/plugins/estree'; -import * as prettier from 'prettier/standalone'; -import {useState, useEffect} from 'react'; +import {useState} from 'react'; import {Resizable} from 're-resizable'; import {useStore} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; @@ -23,30 +19,6 @@ export default function ConfigEditor(): JSX.Element { const [, setMonaco] = useState(null); const store = useStore(); - // Parse string-based override config from pragma comment and format it - const [configJavaScript, setConfigJavaScript] = useState(''); - - useEffect(() => { - const pragma = store.source.substring(0, store.source.indexOf('\n')); - const configString = `(${parseConfigPragmaAsString(pragma)})`; - - prettier - .format(configString, { - semi: true, - parser: 'babel-ts', - plugins: [parserBabel, prettierPluginEstree], - }) - .then(formatted => { - setConfigJavaScript(formatted); - }) - .catch(error => { - console.error('Error formatting config:', error); - setConfigJavaScript('({})'); // Return empty object if not valid for now - //TODO: Add validation and error handling for config - }); - console.log('Config:', configString); - }, [store.source]); - const handleChange: (value: string | undefined) => void = value => { if (!value) return; @@ -81,7 +53,7 @@ export default function ConfigEditor(): JSX.Element { { + dispatchStore({ + type: 'setStore', + payload: { + store: { + ...mountStore, + config, + }, + }, + }); }); }); diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index 5cfa56d77f743..f346f742ed21e 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -7,9 +7,13 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; import {CompilerErrorDetail} from 'babel-plugin-react-compiler'; +import {parseConfigPragmaAsString} from '../../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; import invariant from 'invariant'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; +import parserBabel from 'prettier/plugins/babel'; +import prettierPluginEstree from 'prettier/plugins/estree'; +import * as prettier from 'prettier/standalone'; import {Resizable} from 're-resizable'; import {useEffect, useState} from 'react'; import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics'; @@ -25,6 +29,27 @@ type Props = { language: 'flow' | 'typescript'; }; +// Parse config from pragma and format it with prettier +export async function parseAndFormatConfig(source: string): Promise { + const pragma = source.substring(0, source.indexOf('\n')); + let configString = parseConfigPragmaAsString(pragma); + if (configString !== '') { + configString = `(${configString})`; + } + + try { + const formatted = await prettier.format(configString, { + semi: true, + parser: 'babel-ts', + plugins: [parserBabel, prettierPluginEstree], + }); + return formatted; + } catch (error) { + console.error('Error formatting config:', error); + return ''; // Return empty string if not valid for now + } +} + export default function Input({errors, language}: Props): JSX.Element { const [monaco, setMonaco] = useState(null); const store = useStore(); @@ -79,13 +104,17 @@ export default function Input({errors, language}: Props): JSX.Element { }); }, [monaco, language]); - const handleChange: (value: string | undefined) => void = value => { + const handleChange: (value: string | undefined) => void = async value => { if (!value) return; + // Parse and format the config + const config = await parseAndFormatConfig(value); + dispatchStore({ type: 'updateFile', payload: { source: value, + config, }, }); }; diff --git a/compiler/apps/playground/components/StoreContext.tsx b/compiler/apps/playground/components/StoreContext.tsx index 10ad614b05554..3dfe26cba75ff 100644 --- a/compiler/apps/playground/components/StoreContext.tsx +++ b/compiler/apps/playground/components/StoreContext.tsx @@ -56,6 +56,7 @@ type ReducerAction = type: 'updateFile'; payload: { source: string; + config?: string; }; }; @@ -66,10 +67,11 @@ function storeReducer(store: Store, action: ReducerAction): Store { return newStore; } case 'updateFile': { - const {source} = action.payload; + const {source, config} = action.payload; const newStore = { ...store, source, + config, }; return newStore; } diff --git a/compiler/apps/playground/lib/defaultStore.ts b/compiler/apps/playground/lib/defaultStore.ts index 132ab445e18ce..1031a830fa0d9 100644 --- a/compiler/apps/playground/lib/defaultStore.ts +++ b/compiler/apps/playground/lib/defaultStore.ts @@ -15,8 +15,10 @@ export default function MyApp() { export const defaultStore: Store = { source: index, + config: '', }; export const emptyStore: Store = { source: '', + config: '', }; diff --git a/compiler/apps/playground/lib/stores/store.ts b/compiler/apps/playground/lib/stores/store.ts index ad4a57cf914a9..e37140cb25259 100644 --- a/compiler/apps/playground/lib/stores/store.ts +++ b/compiler/apps/playground/lib/stores/store.ts @@ -17,6 +17,7 @@ import {defaultStore} from '../defaultStore'; */ export interface Store { source: string; + config?: string; } export function encodeStore(store: Store): string { return compressToEncodedURIComponent(JSON.stringify(store)); @@ -65,5 +66,14 @@ export function initStoreFromUrlOrLocalStorage(): Store { const raw = decodeStore(encodedSource); invariant(isValidStore(raw), 'Invalid Store'); + + // Add config property if missing for backwards compatibility + if (!('config' in raw)) { + return { + ...raw, + config: '', + }; + } + return raw; } From 241e5c42d7ad9e17a3337ad10d6650cac918bfb2 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 29 Aug 2025 18:22:33 -0400 Subject: [PATCH 3/4] Add sync from config editor to pragma --- .../components/Editor/ConfigEditor.tsx | 38 ++++++-- .../components/Editor/EditorImpl.tsx | 3 +- .../playground/components/Editor/Input.tsx | 26 +----- compiler/apps/playground/lib/configUtils.ts | 87 +++++++++++++++++++ 4 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 compiler/apps/playground/lib/configUtils.ts diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 31c5b09e281bd..104cea42068d7 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -10,20 +10,47 @@ import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; import {useState} from 'react'; import {Resizable} from 're-resizable'; -import {useStore} from '../StoreContext'; +import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; +import { + generateOverridePragmaFromConfig, + updateSourceWithOverridePragma, +} from '../../lib/configUtils'; loader.config({monaco}); export default function ConfigEditor(): JSX.Element { const [, setMonaco] = useState(null); const store = useStore(); + const dispatchStore = useStoreDispatch(); - const handleChange: (value: string | undefined) => void = value => { - if (!value) return; + const handleChange: (value: string | undefined) => void = async value => { + if (value === undefined) return; - // TODO: Implement sync logic to update pragma comments in the source - console.log('Config changed:', value); + try { + const newPragma = await generateOverridePragmaFromConfig(value); + const updatedSource = updateSourceWithOverridePragma( + store.source, + newPragma, + ); + + // Update the store with both the new config and updated source + dispatchStore({ + type: 'updateFile', + payload: { + source: updatedSource, + config: value, + }, + }); + } catch (error) { + dispatchStore({ + type: 'updateFile', + payload: { + source: store.source, + config: value, + }, + }); + } }; const handleMount: ( @@ -58,7 +85,6 @@ export default function ConfigEditor(): JSX.Element { onChange={handleChange} options={{ ...monacoOptions, - readOnly: true, lineNumbers: 'off', folding: false, renderLineHighlight: 'none', diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index d31b3ab24c6c0..6d7dd73e6d148 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -38,7 +38,7 @@ import { } from '../../lib/stores'; import {useStore, useStoreDispatch} from '../StoreContext'; import ConfigEditor from './ConfigEditor'; -import {default as Input, parseAndFormatConfig} from './Input'; +import Input from './Input'; import { CompilerOutput, CompilerTransformOutput, @@ -48,6 +48,7 @@ import { import {transformFromAstSync} from '@babel/core'; import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint'; import {useSearchParams} from 'next/navigation'; +import {parseAndFormatConfig} from '../../lib/configUtils'; function parseInput( input: string, diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index f346f742ed21e..0441f3f9a4be0 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -7,13 +7,9 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; import {CompilerErrorDetail} from 'babel-plugin-react-compiler'; -import {parseConfigPragmaAsString} from '../../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; import invariant from 'invariant'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; -import parserBabel from 'prettier/plugins/babel'; -import prettierPluginEstree from 'prettier/plugins/estree'; -import * as prettier from 'prettier/standalone'; import {Resizable} from 're-resizable'; import {useEffect, useState} from 'react'; import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics'; @@ -21,6 +17,7 @@ import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack. import React$Types from '../../node_modules/@types/react/index.d.ts'; +import {parseAndFormatConfig} from '../../lib/configUtils.ts'; loader.config({monaco}); @@ -29,27 +26,6 @@ type Props = { language: 'flow' | 'typescript'; }; -// Parse config from pragma and format it with prettier -export async function parseAndFormatConfig(source: string): Promise { - const pragma = source.substring(0, source.indexOf('\n')); - let configString = parseConfigPragmaAsString(pragma); - if (configString !== '') { - configString = `(${configString})`; - } - - try { - const formatted = await prettier.format(configString, { - semi: true, - parser: 'babel-ts', - plugins: [parserBabel, prettierPluginEstree], - }); - return formatted; - } catch (error) { - console.error('Error formatting config:', error); - return ''; // Return empty string if not valid for now - } -} - export default function Input({errors, language}: Props): JSX.Element { const [monaco, setMonaco] = useState(null); const store = useStore(); diff --git a/compiler/apps/playground/lib/configUtils.ts b/compiler/apps/playground/lib/configUtils.ts new file mode 100644 index 0000000000000..e01072d9bafa7 --- /dev/null +++ b/compiler/apps/playground/lib/configUtils.ts @@ -0,0 +1,87 @@ +/** + * 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 parserBabel from 'prettier/plugins/babel'; +import prettierPluginEstree from 'prettier/plugins/estree'; +import * as prettier from 'prettier/standalone'; +import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; + +/** + * Parse config from pragma and format it with prettier + */ +export async function parseAndFormatConfig(source: string): Promise { + const pragma = source.substring(0, source.indexOf('\n')); + let configString = parseConfigPragmaAsString(pragma); + if (configString !== '') { + configString = `(${configString})`; + } + + try { + const formatted = await prettier.format(configString, { + semi: true, + parser: 'babel-ts', + plugins: [parserBabel, prettierPluginEstree], + }); + return formatted; + } catch (error) { + console.error('Error formatting config:', error); + return ''; // Return empty string if not valid for now + } +} + +function extractCurlyBracesContent(input) { + const startIndex = input.indexOf('{'); + const endIndex = input.lastIndexOf('}'); + if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { + throw new Error('No outer curly braces found in input'); + } + return input.slice(startIndex, endIndex + 1); +} + +function cleanContent(content) { + return content + .replace(/[\r\n]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Generate a the override pragma comment from a formatted config object string + */ +export async function generateOverridePragmaFromConfig( + formattedConfigString: string, +): Promise { + const content = extractCurlyBracesContent(formattedConfigString); + const cleanConfig = cleanContent(content); + + // Format the config to ensure it's valid + await prettier.format(`(${cleanConfig})`, { + semi: false, + parser: 'babel-ts', + plugins: [parserBabel, prettierPluginEstree], + }); + + return `// @OVERRIDE:${cleanConfig}`; +} + +/** + * Update the override pragma comment in source code. + */ +export function updateSourceWithOverridePragma( + source: string, + newPragma: string, +): string { + const firstLineEnd = source.indexOf('\n'); + const firstLine = source.substring(0, firstLineEnd); + + const pragmaRegex = /^\/\/\s*@/; + if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) { + return newPragma + source.substring(firstLineEnd); + } else { + return newPragma + '\n' + source; + } +} From 66bff46c2fb1e18cd2e57890f5560d5609fbda72 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 29 Aug 2025 18:25:27 -0400 Subject: [PATCH 4/4] Environment bug fix --- .../playground/components/Editor/ConfigEditor.tsx | 2 +- compiler/apps/playground/lib/configUtils.ts | 4 ++-- .../src/Utils/TestUtils.ts | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 104cea42068d7..ce0e502fac21d 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -42,7 +42,7 @@ export default function ConfigEditor(): JSX.Element { config: value, }, }); - } catch (error) { + } catch (_) { dispatchStore({ type: 'updateFile', payload: { diff --git a/compiler/apps/playground/lib/configUtils.ts b/compiler/apps/playground/lib/configUtils.ts index e01072d9bafa7..d987406f99892 100644 --- a/compiler/apps/playground/lib/configUtils.ts +++ b/compiler/apps/playground/lib/configUtils.ts @@ -33,7 +33,7 @@ export async function parseAndFormatConfig(source: string): Promise { } } -function extractCurlyBracesContent(input) { +function extractCurlyBracesContent(input: string): string { const startIndex = input.indexOf('{'); const endIndex = input.lastIndexOf('}'); if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { @@ -42,7 +42,7 @@ function extractCurlyBracesContent(input) { return input.slice(startIndex, endIndex + 1); } -function cleanContent(content) { +function cleanContent(content: string): string { return content .replace(/[\r\n]+/g, ' ') .replace(/\s+/g, ' ') diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts index 172270a6d0c79..c384165c31260 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -255,11 +255,16 @@ function parseConfigStringAsJS( console.log('OVERRIDE:', parsedConfig); + const environment = parseConfigPragmaEnvironmentForTest( + '', + defaults.environment ?? {}, + ); + const options: Record = { ...defaultOptions, panicThreshold: 'all_errors', compilationMode: defaults.compilationMode, - environment: defaults.environment ?? defaultOptions.environment, + environment, }; // Apply parsed config, merging environment if it exists @@ -281,10 +286,6 @@ function parseConfigStringAsJS( }); } - if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) { - validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false; - } - options.environment = validatedEnvironment.data; }