@samitouri / QOS-React / commits / fe84397e81

[compiler][playground] (4/N) Config override panel (#34436)

<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary Removed the old `OVERRIDE` pragma to make the source of truth for config overrides in the left-hand pane. Now, it will automatically update the output pane each time there is an edit to the config. The old pragma format is still supported, but it will be overwritten by the config pane if they are modifying the same flags. Removed the gating on the config panel so now all users will automatically be able to view it, but it will be initially collapsed. <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> ## How did you test this change? https://github.com/user-attachments/assets/9d4512b9-e203-4ce0-ae95-dd96ff03bbc1 <!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. How exactly did you verify that your PR solves the issue you wanted to solve? If you leave this empty, your PR will very likely be closed. -->

Eugene Choi committed Sep 11, 2025 at 11:51 UTC fe84397e81c94a7bccdf0479994a7d0363a12115
7 files changed +80 -355
compiler/apps/playground/components/Editor/ConfigEditor.tsx
+35 -94
@@ -10,14 +10,8 @@ import type {editor} from 'monaco-editor';
10 import * as monaco from 'monaco-editor';
11 import React, {useState, useCallback} from 'react';
12 import {Resizable} from 're-resizable';
13 -import {useSnackbar} from 'notistack';
13 import {useStore, useStoreDispatch} from '../StoreContext';
14 import {monacoOptions} from './monacoOptions';
16 -import {
17 - ConfigError,
18 - generateOverridePragmaFromConfig,
19 - updateSourceWithOverridePragma,
20 -} from '../../lib/configUtils';
15
16 // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
17 import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
@@ -28,61 +22,17 @@ export default function ConfigEditor(): React.ReactElement {
22 const [isExpanded, setIsExpanded] = useState(false);
23 const store = useStore();
24 const dispatchStore = useStoreDispatch();
31 - const {enqueueSnackbar} = useSnackbar();
25
26 const toggleExpanded = useCallback(() => {
27 setIsExpanded(prev => !prev);
28 }, []);
29
37 - const handleApplyConfig: () => Promise<void> = async () => {
38 - try {
39 - const config = store.config || '';
40 -
41 - if (!config.trim()) {
42 - enqueueSnackbar(
43 - 'Config is empty. Please add configuration options first.',
44 - {
45 - variant: 'warning',
46 - },
47 - );
48 - return;
49 - }
50 - const newPragma = await generateOverridePragmaFromConfig(config);
51 - const updatedSource = updateSourceWithOverridePragma(
52 - store.source,
53 - newPragma,
54 - );
55 -
56 - dispatchStore({
57 - type: 'updateFile',
58 - payload: {
59 - source: updatedSource,
60 - config: config,
61 - },
62 - });
63 - } catch (error) {
64 - console.error('Failed to apply config:', error);
65 -
66 - if (error instanceof ConfigError && error.message.trim()) {
67 - enqueueSnackbar(error.message, {
68 - variant: 'error',
69 - });
70 - } else {
71 - enqueueSnackbar('Unexpected error: failed to apply config.', {
72 - variant: 'error',
73 - });
74 - }
75 - }
76 - };
77 -
30 const handleChange: (value: string | undefined) => void = value => {
31 if (value === undefined) return;
32
81 - // Only update the config
33 dispatchStore({
83 - type: 'updateFile',
34 + type: 'updateConfig',
35 payload: {
85 - source: store.source,
36 config: value,
37 },
38 });
@@ -120,49 +70,40 @@ export default function ConfigEditor(): React.ReactElement {
70 return (
71 <div className="flex flex-row relative">
72 {isExpanded ? (
123 - <>
124 - <Resizable
125 - className="border-r"
126 - minWidth={300}
127 - maxWidth={600}
128 - defaultSize={{width: 350, height: 'auto'}}
129 - enable={{right: true}}>
130 - <h2
131 - title="Minimize config editor"
132 - aria-label="Minimize config editor"
133 - onClick={toggleExpanded}
134 - className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
135 - - Config Overrides
136 - </h2>
137 - <div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
138 - <MonacoEditor
139 - path={'config.ts'}
140 - language={'typescript'}
141 - value={store.config}
142 - onMount={handleMount}
143 - onChange={handleChange}
144 - options={{
145 - ...monacoOptions,
146 - lineNumbers: 'off',
147 - folding: false,
148 - renderLineHighlight: 'none',
149 - scrollBeyondLastLine: false,
150 - hideCursorInOverviewRuler: true,
151 - overviewRulerBorder: false,
152 - overviewRulerLanes: 0,
153 - fontSize: 12,
154 - }}
155 - />
156 - </div>
157 - </Resizable>
158 - <button
159 - onClick={handleApplyConfig}
160 - title="Apply config overrides to input"
161 - aria-label="Apply config overrides to input"
162 - className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
163 - →
164 - </button>
165 - </>
73 + <Resizable
74 + className="border-r"
75 + minWidth={300}
76 + maxWidth={600}
77 + defaultSize={{width: 350, height: 'auto'}}
78 + enable={{right: true}}>
79 + <h2
80 + title="Minimize config editor"
81 + aria-label="Minimize config editor"
82 + onClick={toggleExpanded}
83 + className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
84 + - Config Overrides
85 + </h2>
86 + <div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
87 + <MonacoEditor
88 + path={'config.ts'}
89 + language={'typescript'}
90 + value={store.config}
91 + onMount={handleMount}
92 + onChange={handleChange}
93 + options={{
94 + ...monacoOptions,
95 + lineNumbers: 'off',
96 + folding: false,
97 + renderLineHighlight: 'none',
98 + scrollBeyondLastLine: false,
99 + hideCursorInOverviewRuler: true,
100 + overviewRulerBorder: false,
101 + overviewRulerLanes: 0,
102 + fontSize: 12,
103 + }}
104 + />
105 + </div>
106 + </Resizable>
107 ) : (
108 <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
109 <button
compiler/apps/playground/components/Editor/EditorImpl.tsx
+26 -14
@@ -22,6 +22,7 @@ import BabelPluginReactCompiler, {
22 parsePluginOptions,
23 printReactiveFunctionWithOutlined,
24 printFunctionWithOutlined,
25 + type LoggerEvent,
26 } from 'babel-plugin-react-compiler';
27 import clsx from 'clsx';
28 import invariant from 'invariant';
@@ -46,7 +47,6 @@ import {
47 PrintedCompilerPipelineValue,
48 } from './Output';
49 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(
@@ -147,6 +147,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
147 function compile(
148 source: string,
149 mode: 'compiler' | 'linter',
150 + configOverrides: string,
151 ): [CompilerOutput, 'flow' | 'typescript'] {
152 const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
153 const error = new CompilerError();
@@ -207,7 +208,7 @@ function compile(
208 }
209 }
210 };
210 - const parsedOptions = parseConfigPragmaForTests(pragma, {
211 + const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
212 compilationMode: 'infer',
213 environment:
214 mode === 'linter'
@@ -227,10 +228,26 @@ function compile(
228 /* use defaults for compiler mode */
229 },
230 });
231 +
232 + // Parse config overrides from config editor
233 + let configOverrideOptions: any = {};
234 + const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
235 + // TODO: initialize store with URL params, not empty store
236 + if (configOverrides.trim()) {
237 + if (configMatch && configMatch[1]) {
238 + const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
239 + configOverrideOptions = new Function(`return (${configString})`)();
240 + } else {
241 + throw new Error('Invalid config overrides');
242 + }
243 + }
244 +
245 const opts: PluginOptions = parsePluginOptions({
231 - ...parsedOptions,
246 + ...parsedPragmaOptions,
247 + ...configOverrideOptions,
248 environment: {
233 - ...parsedOptions.environment,
249 + ...parsedPragmaOptions.environment,
250 + ...configOverrideOptions.environment,
251 customHooks: new Map([...COMMON_HOOKS]),
252 },
253 logger: {
@@ -285,19 +302,14 @@ export default function Editor(): JSX.Element {
302 const dispatchStore = useStoreDispatch();
303 const {enqueueSnackbar} = useSnackbar();
304 const [compilerOutput, language] = useMemo(
288 - () => compile(deferredStore.source, 'compiler'),
289 - [deferredStore.source],
305 + () => compile(deferredStore.source, 'compiler', deferredStore.config),
306 + [deferredStore.source, deferredStore.config],
307 );
308 const [linterOutput] = useMemo(
292 - () => compile(deferredStore.source, 'linter'),
293 - [deferredStore.source],
309 + () => compile(deferredStore.source, 'linter', deferredStore.config),
310 + [deferredStore.source, deferredStore.config],
311 );
312
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 -
313 useMountEffect(() => {
314 // Initialize store
315 let mountStore: Store;
@@ -339,7 +351,7 @@ export default function Editor(): JSX.Element {
351 return (
352 <>
353 <div className="relative flex basis top-14">
342 - {shouldShowConfig && <ConfigEditor />}
354 + <ConfigEditor />
355 <div className={clsx('relative sm:basis-1/4')}>
356 <Input language={language} errors={errors} />
357 </div>
compiler/apps/playground/components/Editor/Input.tsx
+1 -6
@@ -17,7 +17,6 @@ import {useStore, useStoreDispatch} from '../StoreContext';
17 import {monacoOptions} from './monacoOptions';
18 // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
19 import React$Types from '../../node_modules/@types/react/index.d.ts';
20 -import {parseAndFormatConfig} from '../../lib/configUtils.ts';
20
21 loader.config({monaco});
22
@@ -83,14 +82,10 @@ export default function Input({errors, language}: Props): JSX.Element {
82 const handleChange: (value: string | undefined) => void = async value => {
83 if (!value) return;
84
86 - // Parse and format the config
87 - const config = await parseAndFormatConfig(value);
88 -
85 dispatchStore({
90 - type: 'updateFile',
86 + type: 'updateSource',
87 payload: {
88 source: value,
93 - config,
89 },
90 });
91 };
compiler/apps/playground/components/StoreContext.tsx
+15 -3
@@ -53,9 +53,14 @@ type ReducerAction =
53 };
54 }
55 | {
56 - type: 'updateFile';
56 + type: 'updateSource';
57 payload: {
58 source: string;
59 + };
60 + }
61 + | {
62 + type: 'updateConfig';
63 + payload: {
64 config: string;
65 };
66 }
@@ -69,11 +74,18 @@ function storeReducer(store: Store, action: ReducerAction): Store {
74 const newStore = action.payload.store;
75 return newStore;
76 }
72 - case 'updateFile': {
73 - const {source, config} = action.payload;
77 + case 'updateSource': {
78 + const source = action.payload.source;
79 const newStore = {
80 ...store,
81 source,
82 + };
83 + return newStore;
84 + }
85 + case 'updateConfig': {
86 + const config = action.payload.config;
87 + const newStore = {
88 + ...store,
89 config,
90 };
91 return newStore;
compiler/apps/playground/lib/configUtils.ts deleted
-120
@@ -1,120 +0,0 @@
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 parserBabel from 'prettier/plugins/babel';
9 -import prettierPluginEstree from 'prettier/plugins/estree';
10 -import * as prettier from 'prettier/standalone';
11 -import {parsePluginOptions} from 'babel-plugin-react-compiler';
12 -import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
13 -
14 -export class ConfigError extends Error {
15 - constructor(message: string) {
16 - super(message);
17 - this.name = 'ConfigError';
18 - }
19 -}
20 -/**
21 - * Parse config from pragma and format it with prettier
22 - */
23 -export async function parseAndFormatConfig(source: string): Promise<string> {
24 - const pragma = source.substring(0, source.indexOf('\n'));
25 - let configString = parseConfigPragmaAsString(pragma);
26 - if (configString !== '') {
27 - configString = `\
28 - import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
29 -
30 - (${configString} satisfies Partial<PluginOptions>)`;
31 - }
32 -
33 - try {
34 - const formatted = await prettier.format(configString, {
35 - semi: true,
36 - parser: 'babel-ts',
37 - plugins: [parserBabel, prettierPluginEstree],
38 - });
39 - return formatted;
40 - } catch (error) {
41 - console.error('Error formatting config:', error);
42 - return ''; // Return empty string if not valid for now
43 - }
44 -}
45 -
46 -function extractCurlyBracesContent(input: string): string {
47 - const startIndex = input.indexOf('({') + 1;
48 - const endIndex = input.lastIndexOf('}');
49 - if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
50 - throw new Error('No outer curly braces found in input.');
51 - }
52 - return input.slice(startIndex, endIndex + 1);
53 -}
54 -
55 -function cleanContent(content: string): string {
56 - return content
57 - .replace(/[\r\n]+/g, ' ')
58 - .replace(/\s+/g, ' ')
59 - .trim();
60 -}
61 -
62 -/**
63 - * Validate that a config string can be parsed as a valid PluginOptions object
64 - * Throws an error if validation fails.
65 - */
66 -function validateConfigAsPluginOptions(configString: string): void {
67 - // Validate that config can be parse as JS obj
68 - let parsedConfig: unknown;
69 - try {
70 - parsedConfig = new Function(`return (${configString})`)();
71 - } catch (_) {
72 - throw new ConfigError('Config has invalid syntax.');
73 - }
74 -
75 - // Validate against PluginOptions schema
76 - try {
77 - parsePluginOptions(parsedConfig);
78 - } catch (_) {
79 - throw new ConfigError('Config does not match the expected schema.');
80 - }
81 -}
82 -
83 -/**
84 - * Generate a the override pragma comment from a formatted config object string
85 - */
86 -export async function generateOverridePragmaFromConfig(
87 - formattedConfigString: string,
88 -): Promise<string> {
89 - const content = extractCurlyBracesContent(formattedConfigString);
90 - const cleanConfig = cleanContent(content);
91 -
92 - validateConfigAsPluginOptions(cleanConfig);
93 -
94 - // Format the config to ensure it's valid
95 - await prettier.format(`(${cleanConfig})`, {
96 - semi: false,
97 - parser: 'babel-ts',
98 - plugins: [parserBabel, prettierPluginEstree],
99 - });
100 -
101 - return `// @OVERRIDE:${cleanConfig}`;
102 -}
103 -
104 -/**
105 - * Update the override pragma comment in source code.
106 - */
107 -export function updateSourceWithOverridePragma(
108 - source: string,
109 - newPragma: string,
110 -): string {
111 - const firstLineEnd = source.indexOf('\n');
112 - const firstLine = source.substring(0, firstLineEnd);
113 -
114 - const pragmaRegex = /^\/\/\s*@/;
115 - if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
116 - return newPragma + source.substring(firstLineEnd);
117 - } else {
118 - return newPragma + '\n' + source;
119 - }
120 -}
compiler/apps/playground/lib/defaultStore.ts
+3 -16
@@ -17,22 +17,9 @@ export const defaultConfig = `\
17 import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
18
19 ({
20 - compilationMode: 'infer',
21 - panicThreshold: 'none',
22 - environment: {},
23 - logger: null,
24 - gating: null,
25 - noEmit: false,
26 - dynamicGating: null,
27 - eslintSuppressionRules: null,
28 - flowSuppressions: true,
29 - ignoreUseNoForget: false,
30 - sources: filename => {
31 - return filename.indexOf('node_modules') === -1;
32 - },
33 - enableReanimatedCheck: true,
34 - customOptOutDirectives: null,
35 - target: '19',
20 + environment: {
21 + enableResetCacheOnSourceFileChanges: false
22 + }
23 } satisfies Partial<PluginOptions>);`;
24
25 export const defaultStore: Store = {
compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
-102
@@ -188,11 +188,6 @@ export function parseConfigPragmaForTests(
188 environment?: PartialEnvironmentConfig;
189 },
190 ): PluginOptions {
191 - const overridePragma = parseConfigPragmaAsString(pragma);
192 - if (overridePragma !== '') {
193 - return parseConfigStringAsJS(overridePragma, defaults);
194 - }
195 -
191 const environment = parseConfigPragmaEnvironmentForTest(
192 pragma,
193 defaults.environment ?? {},
@@ -228,100 +223,3 @@ export function parseConfigPragmaForTests(
223 }
224 return parsePluginOptions(options);
225 }
231 -
232 -export function parseConfigPragmaAsString(pragma: string): string {
233 - // Check if it's in JS override format
234 - for (const {key, value: val} of splitPragma(pragma)) {
235 - if (key === 'OVERRIDE' && val != null) {
236 - return val;
237 - }
238 - }
239 - return '';
240 -}
241 -
242 -function parseConfigStringAsJS(
243 - configString: string,
244 - defaults: {
245 - compilationMode: CompilationMode;
246 - environment?: PartialEnvironmentConfig;
247 - },
248 -): PluginOptions {
249 - let parsedConfig: any;
250 - try {
251 - // Parse the JavaScript object literal
252 - parsedConfig = new Function(`return ${configString}`)();
253 - } catch (error) {
254 - CompilerError.invariant(false, {
255 - reason: 'Failed to parse config pragma as JavaScript object',
256 - description: `Could not parse: ${configString}. Error: ${error}`,
257 - details: [
258 - {
259 - kind: 'error',
260 - loc: null,
261 - message: null,
262 - },
263 - ],
264 - suggestions: null,
265 - });
266 - }
267 -
268 - const environment = parseConfigPragmaEnvironmentForTest(
269 - '',
270 - defaults.environment ?? {},
271 - );
272 -
273 - const options: Record<keyof PluginOptions, unknown> = {
274 - ...defaultOptions,
275 - panicThreshold: 'all_errors',
276 - compilationMode: defaults.compilationMode,
277 - environment,
278 - };
279 -
280 - // Apply parsed config, merging environment if it exists
281 - if (parsedConfig.environment) {
282 - const mergedEnvironment = {
283 - ...(options.environment as Record<string, unknown>),
284 - ...parsedConfig.environment,
285 - };
286 -
287 - // Validate environment config
288 - const validatedEnvironment =
289 - EnvironmentConfigSchema.safeParse(mergedEnvironment);
290 - if (!validatedEnvironment.success) {
291 - CompilerError.invariant(false, {
292 - reason: 'Invalid environment configuration in config pragma',
293 - description: `${fromZodError(validatedEnvironment.error)}`,
294 - details: [
295 - {
296 - kind: 'error',
297 - loc: null,
298 - message: null,
299 - },
300 - ],
301 - suggestions: null,
302 - });
303 - }
304 -
305 - options.environment = validatedEnvironment.data;
306 - }
307 -
308 - // Apply other config options
309 - for (const [key, value] of Object.entries(parsedConfig)) {
310 - if (key === 'environment') {
311 - continue;
312 - }
313 -
314 - if (hasOwnProperty(defaultOptions, key)) {
315 - if (key === 'target' && value === 'donotuse_meta_internal') {
316 - options[key] = {
317 - kind: value,
318 - runtimeModule: 'react',
319 - };
320 - } else {
321 - options[key] = value;
322 - }
323 - }
324 - }
325 -
326 - return parsePluginOptions(options);
327 -}