diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index d8b99c1dea4e4..ce0e502fac21d 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -6,52 +6,51 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler'; 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 {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(); - // Parse string-based override config from pragma comment and format it - const [configJavaScript, setConfigJavaScript] = useState(''); + const handleChange: (value: string | undefined) => void = async value => { + if (value === undefined) return; - useEffect(() => { - const pragma = store.source.substring(0, store.source.indexOf('\n')); - const configString = `(${parseConfigPragmaAsString(pragma)})`; + try { + const newPragma = await generateOverridePragmaFromConfig(value); + const updatedSource = updateSourceWithOverridePragma( + store.source, + newPragma, + ); - 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 + // Update the store with both the new config and updated source + dispatchStore({ + type: 'updateFile', + payload: { + source: updatedSource, + config: value, + }, }); - console.log('Config:', configString); - }, [store.source]); - - const handleChange: (value: string | undefined) => void = value => { - if (!value) return; - - // TODO: Implement sync logic to update pragma comments in the source - console.log('Config changed:', value); + } catch (_) { + dispatchStore({ + type: 'updateFile', + payload: { + source: store.source, + config: value, + }, + }); + } }; const handleMount: ( @@ -81,12 +80,11 @@ 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..0441f3f9a4be0 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -17,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}); @@ -79,13 +80,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/configUtils.ts b/compiler/apps/playground/lib/configUtils.ts new file mode 100644 index 0000000000000..d987406f99892 --- /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: string): string { + 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: string): string { + 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; + } +} 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; } 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..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 @@ -269,22 +274,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', @@ -294,10 +286,6 @@ function parseConfigStringAsJS( }); } - if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) { - validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false; - } - options.environment = validatedEnvironment.data; } @@ -308,9 +296,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; }