Skip to content

Commit c5362a3

Browse files
authored
[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" />
1 parent 89a803f commit c5362a3

File tree

4 files changed

+222
-1
lines changed

4 files changed

+222
-1
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
type Store,
3838
} from '../../lib/stores';
3939
import {useStore, useStoreDispatch} from '../StoreContext';
40+
import ConfigEditor from './ConfigEditor';
4041
import Input from './Input';
4142
import {
4243
CompilerOutput,
@@ -46,6 +47,7 @@ import {
4647
} from './Output';
4748
import {transformFromAstSync} from '@babel/core';
4849
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
50+
import {useSearchParams} from 'next/navigation';
4951

5052
function parseInput(
5153
input: string,
@@ -291,7 +293,13 @@ export default function Editor(): JSX.Element {
291293
[deferredStore.source],
292294
);
293295

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+
294301
useMountEffect(() => {
302+
// Initialize store
295303
let mountStore: Store;
296304
try {
297305
mountStore = initStoreFromUrlOrLocalStorage();
@@ -328,6 +336,7 @@ export default function Editor(): JSX.Element {
328336
return (
329337
<>
330338
<div className="relative flex basis top-14">
339+
{shouldShowConfig && <ConfigEditor />}
331340
<div className={clsx('relative sm:basis-1/4')}>
332341
<Input language={language} errors={errors} />
333342
</div>

compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ export function parseConfigPragmaForTests(
182182
environment?: PartialEnvironmentConfig;
183183
},
184184
): PluginOptions {
185+
const overridePragma = parseConfigPragmaAsString(pragma);
186+
if (overridePragma !== '') {
187+
return parseConfigStringAsJS(overridePragma, defaults);
188+
}
189+
185190
const environment = parseConfigPragmaEnvironmentForTest(
186191
pragma,
187192
defaults.environment ?? {},
@@ -217,3 +222,104 @@ export function parseConfigPragmaForTests(
217222
}
218223
return parsePluginOptions(options);
219224
}
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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export {
4848
printReactiveFunction,
4949
printReactiveFunctionWithOutlined,
5050
} from './ReactiveScopes';
51-
export {parseConfigPragmaForTests} from './Utils/TestUtils';
51+
export {
52+
parseConfigPragmaForTests,
53+
parseConfigPragmaAsString,
54+
} from './Utils/TestUtils';
5255
declare global {
5356
let __DEV__: boolean | null | undefined;
5457
}

0 commit comments

Comments
 (0)