@samitouri / QOS-React / commits / c5362a380f

[compiler][playground] (1/N) Config override panel (#34303)

## Summary Part 1 of adding a "Config Override" panel to the React compiler playground. The panel is placed to the left of the current input section, and supports converting the comment pragmas in the input section to a JavaScript-based config. Backwards sync has not been implemented yet. NOTE: I have added support for a new `OVERRIDE` type pragma to add support for Map and Function types. (For now, the old pragma format is still intact) ## Testing Example of the config overrides synced to the source code: <img width="1542" height="527" alt="Screenshot 2025-08-28 at 3 38 13 PM" src="https://github.com/user-attachments/assets/d46e7660-61b9-4145-93b5-a4005d30064a" />

Eugene Choi committed Aug 28, 2025 at 16:26 UTC c5362a380fceab6dd62c7fea18c8413647ee44e3
4 files changed +222 -1
compiler/apps/playground/components/Editor/ConfigEditor.tsx new
+103
@@ -0,0 +1,103 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9 +import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
10 +import type {editor} from 'monaco-editor';
11 +import * as monaco from 'monaco-editor';
12 +import parserBabel from 'prettier/plugins/babel';
13 +import * as prettierPluginEstree from 'prettier/plugins/estree';
14 +import * as prettier from 'prettier/standalone';
15 +import {useState, useEffect} from 'react';
16 +import {Resizable} from 're-resizable';
17 +import {useStore} from '../StoreContext';
18 +import {monacoOptions} from './monacoOptions';
19 +
20 +loader.config({monaco});
21 +
22 +export default function ConfigEditor(): JSX.Element {
23 + const [, setMonaco] = useState<Monaco | null>(null);
24 + const store = useStore();
25 +
26 + // Parse string-based override config from pragma comment and format it
27 + const [configJavaScript, setConfigJavaScript] = useState('');
28 +
29 + useEffect(() => {
30 + const pragma = store.source.substring(0, store.source.indexOf('\n'));
31 + const configString = `(${parseConfigPragmaAsString(pragma)})`;
32 +
33 + prettier
34 + .format(configString, {
35 + semi: true,
36 + parser: 'babel-ts',
37 + plugins: [parserBabel, prettierPluginEstree],
38 + })
39 + .then(formatted => {
40 + setConfigJavaScript(formatted);
41 + })
42 + .catch(error => {
43 + console.error('Error formatting config:', error);
44 + setConfigJavaScript('({})'); // Return empty object if not valid for now
45 + //TODO: Add validation and error handling for config
46 + });
47 + console.log('Config:', configString);
48 + }, [store.source]);
49 +
50 + const handleChange: (value: string | undefined) => void = value => {
51 + if (!value) return;
52 +
53 + // TODO: Implement sync logic to update pragma comments in the source
54 + console.log('Config changed:', value);
55 + };
56 +
57 + const handleMount: (
58 + _: editor.IStandaloneCodeEditor,
59 + monaco: Monaco,
60 + ) => void = (_, monaco) => {
61 + setMonaco(monaco);
62 +
63 + const uri = monaco.Uri.parse(`file:///config.js`);
64 + const model = monaco.editor.getModel(uri);
65 + if (model) {
66 + model.updateOptions({tabSize: 2});
67 + }
68 + };
69 +
70 + return (
71 + <div className="relative flex flex-col flex-none border-r border-gray-200">
72 + <h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
73 + Config Overrides
74 + </h2>
75 + <Resizable
76 + minWidth={300}
77 + maxWidth={600}
78 + defaultSize={{width: 350, height: 'auto'}}
79 + enable={{right: true}}
80 + className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
81 + <MonacoEditor
82 + path={'config.js'}
83 + language={'javascript'}
84 + value={configJavaScript}
85 + onMount={handleMount}
86 + onChange={handleChange}
87 + options={{
88 + ...monacoOptions,
89 + readOnly: true,
90 + lineNumbers: 'off',
91 + folding: false,
92 + renderLineHighlight: 'none',
93 + scrollBeyondLastLine: false,
94 + hideCursorInOverviewRuler: true,
95 + overviewRulerBorder: false,
96 + overviewRulerLanes: 0,
97 + fontSize: 12,
98 + }}
99 + />
100 + </Resizable>
101 + </div>
102 + );
103 +}
compiler/apps/playground/components/Editor/EditorImpl.tsx
+9
@@ -37,6 +37,7 @@ import {
37 type Store,
38 } from '../../lib/stores';
39 import {useStore, useStoreDispatch} from '../StoreContext';
40 +import ConfigEditor from './ConfigEditor';
41 import Input from './Input';
42 import {
43 CompilerOutput,
@@ -46,6 +47,7 @@ import {
47 } from './Output';
48 import {transformFromAstSync} from '@babel/core';
49 import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
50 +import {useSearchParams} from 'next/navigation';
51
52 function parseInput(
53 input: string,
@@ -291,7 +293,13 @@ export default function Editor(): JSX.Element {
293 [deferredStore.source],
294 );
295
296 + // TODO: Remove this once the config editor is more stable
297 + const searchParams = useSearchParams();
298 + const search = searchParams.get('showConfig');
299 + const shouldShowConfig = search === 'true';
300 +
301 useMountEffect(() => {
302 + // Initialize store
303 let mountStore: Store;
304 try {
305 mountStore = initStoreFromUrlOrLocalStorage();
@@ -328,6 +336,7 @@ export default function Editor(): JSX.Element {
336 return (
337 <>
338 <div className="relative flex basis top-14">
339 + {shouldShowConfig && <ConfigEditor />}
340 <div className={clsx('relative sm:basis-1/4')}>
341 <Input language={language} errors={errors} />
342 </div>
compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
+106
@@ -182,6 +182,11 @@ export function parseConfigPragmaForTests(
182 environment?: PartialEnvironmentConfig;
183 },
184 ): PluginOptions {
185 + const overridePragma = parseConfigPragmaAsString(pragma);
186 + if (overridePragma !== '') {
187 + return parseConfigStringAsJS(overridePragma, defaults);
188 + }
189 +
190 const environment = parseConfigPragmaEnvironmentForTest(
191 pragma,
192 defaults.environment ?? {},
@@ -217,3 +222,104 @@ export function parseConfigPragmaForTests(
222 }
223 return parsePluginOptions(options);
224 }
225 +
226 +export function parseConfigPragmaAsString(pragma: string): string {
227 + // Check if it's in JS override format
228 + for (const {key, value: val} of splitPragma(pragma)) {
229 + if (key === 'OVERRIDE' && val != null) {
230 + return val;
231 + }
232 + }
233 + return '';
234 +}
235 +
236 +function parseConfigStringAsJS(
237 + configString: string,
238 + defaults: {
239 + compilationMode: CompilationMode;
240 + environment?: PartialEnvironmentConfig;
241 + },
242 +): PluginOptions {
243 + let parsedConfig: any;
244 + try {
245 + // Parse the JavaScript object literal
246 + parsedConfig = new Function(`return ${configString}`)();
247 + } catch (error) {
248 + CompilerError.invariant(false, {
249 + reason: 'Failed to parse config pragma as JavaScript object',
250 + description: `Could not parse: ${configString}. Error: ${error}`,
251 + loc: null,
252 + suggestions: null,
253 + });
254 + }
255 +
256 + console.log('OVERRIDE:', parsedConfig);
257 +
258 + const options: Record<keyof PluginOptions, unknown> = {
259 + ...defaultOptions,
260 + panicThreshold: 'all_errors',
261 + compilationMode: defaults.compilationMode,
262 + environment: defaults.environment ?? defaultOptions.environment,
263 + };
264 +
265 + // Apply parsed config, merging environment if it exists
266 + if (parsedConfig.environment) {
267 + const mergedEnvironment = {
268 + ...(options.environment as Record<string, unknown>),
269 + ...parsedConfig.environment,
270 + };
271 +
272 + // Apply complex defaults for environment flags that are set to true
273 + const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
274 + {};
275 + for (const [key, value] of Object.entries(mergedEnvironment)) {
276 + if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
277 + if (value === true && key in testComplexConfigDefaults) {
278 + environmentConfig[key] = testComplexConfigDefaults[key];
279 + } else {
280 + environmentConfig[key] = value;
281 + }
282 + }
283 + }
284 +
285 + // Validate environment config
286 + const validatedEnvironment =
287 + EnvironmentConfigSchema.safeParse(environmentConfig);
288 + if (!validatedEnvironment.success) {
289 + CompilerError.invariant(false, {
290 + reason: 'Invalid environment configuration in config pragma',
291 + description: `${fromZodError(validatedEnvironment.error)}`,
292 + loc: null,
293 + suggestions: null,
294 + });
295 + }
296 +
297 + if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
298 + validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
299 + }
300 +
301 + options.environment = validatedEnvironment.data;
302 + }
303 +
304 + // Apply other config options
305 + for (const [key, value] of Object.entries(parsedConfig)) {
306 + if (key === 'environment') {
307 + continue;
308 + }
309 +
310 + if (hasOwnProperty(defaultOptions, key)) {
311 + if (value === true && key in testComplexPluginOptionDefaults) {
312 + options[key] = testComplexPluginOptionDefaults[key];
313 + } else if (key === 'target' && value === 'donotuse_meta_internal') {
314 + options[key] = {
315 + kind: value,
316 + runtimeModule: 'react',
317 + };
318 + } else {
319 + options[key] = value;
320 + }
321 + }
322 + }
323 +
324 + return parsePluginOptions(options);
325 +}
compiler/packages/babel-plugin-react-compiler/src/index.ts
+4 -1
@@ -48,7 +48,10 @@ export {
48 printReactiveFunction,
49 printReactiveFunctionWithOutlined,
50 } from './ReactiveScopes';
51 -export {parseConfigPragmaForTests} from './Utils/TestUtils';
51 +export {
52 + parseConfigPragmaForTests,
53 + parseConfigPragmaAsString,
54 +} from './Utils/TestUtils';
55 declare global {
56 let __DEV__: boolean | null | undefined;
57 }