Skip to content

Commit de5a1b2

Browse files
authored
[compiler][playground] (3/N) Config override panel (#34371)
<!-- 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 Part 3 of adding a "Config Override" panel to the React compiler playground. Added a button to apply config changes to the Input panel, as well as making the tab collapsible. Added validation for the the PluginOptions type (although comes with a bit more boilerplate) to make it very obvious what the possible config errors could be. Added some toasts for trying to apply broken configs. <!-- 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/63ab8636-396f-45ba-aaa5-4136e62ccccc <!-- 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. -->
1 parent b9a0453 commit de5a1b2

File tree

6 files changed

+196
-67
lines changed

6 files changed

+196
-67
lines changed

compiler/apps/playground/components/Editor/ConfigEditor.tsx

Lines changed: 129 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,94 +8,176 @@
88
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
99
import type {editor} from 'monaco-editor';
1010
import * as monaco from 'monaco-editor';
11-
import {useState} from 'react';
11+
import React, {useState, useCallback} from 'react';
1212
import {Resizable} from 're-resizable';
13+
import {useSnackbar} from 'notistack';
1314
import {useStore, useStoreDispatch} from '../StoreContext';
1415
import {monacoOptions} from './monacoOptions';
1516
import {
17+
ConfigError,
1618
generateOverridePragmaFromConfig,
1719
updateSourceWithOverridePragma,
1820
} from '../../lib/configUtils';
1921

22+
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
23+
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
24+
2025
loader.config({monaco});
2126

22-
export default function ConfigEditor(): JSX.Element {
23-
const [, setMonaco] = useState<Monaco | null>(null);
27+
export default function ConfigEditor(): React.ReactElement {
28+
const [isExpanded, setIsExpanded] = useState(false);
2429
const store = useStore();
2530
const dispatchStore = useStoreDispatch();
31+
const {enqueueSnackbar} = useSnackbar();
2632

27-
const handleChange: (value: string | undefined) => void = async value => {
28-
if (value === undefined) return;
33+
const toggleExpanded = useCallback(() => {
34+
setIsExpanded(prev => !prev);
35+
}, []);
2936

37+
const handleApplyConfig: () => Promise<void> = async () => {
3038
try {
31-
const newPragma = await generateOverridePragmaFromConfig(value);
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);
3251
const updatedSource = updateSourceWithOverridePragma(
3352
store.source,
3453
newPragma,
3554
);
3655

37-
// Update the store with both the new config and updated source
3856
dispatchStore({
3957
type: 'updateFile',
4058
payload: {
4159
source: updatedSource,
42-
config: value,
43-
},
44-
});
45-
} catch (_) {
46-
dispatchStore({
47-
type: 'updateFile',
48-
payload: {
49-
source: store.source,
50-
config: value,
60+
config: config,
5161
},
5262
});
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+
}
5375
}
5476
};
5577

78+
const handleChange: (value: string | undefined) => void = value => {
79+
if (value === undefined) return;
80+
81+
// Only update the config
82+
dispatchStore({
83+
type: 'updateFile',
84+
payload: {
85+
source: store.source,
86+
config: value,
87+
},
88+
});
89+
};
90+
5691
const handleMount: (
5792
_: editor.IStandaloneCodeEditor,
5893
monaco: Monaco,
5994
) => void = (_, monaco) => {
60-
setMonaco(monaco);
95+
// Add the babel-plugin-react-compiler type definitions to Monaco
96+
monaco.languages.typescript.typescriptDefaults.addExtraLib(
97+
//@ts-expect-error - compilerTypeDefs is a string
98+
compilerTypeDefs,
99+
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
100+
);
101+
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
102+
target: monaco.languages.typescript.ScriptTarget.Latest,
103+
allowNonTsExtensions: true,
104+
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
105+
module: monaco.languages.typescript.ModuleKind.ESNext,
106+
noEmit: true,
107+
strict: false,
108+
esModuleInterop: true,
109+
allowSyntheticDefaultImports: true,
110+
jsx: monaco.languages.typescript.JsxEmit.React,
111+
});
61112

62-
const uri = monaco.Uri.parse(`file:///config.js`);
113+
const uri = monaco.Uri.parse(`file:///config.ts`);
63114
const model = monaco.editor.getModel(uri);
64115
if (model) {
65116
model.updateOptions({tabSize: 2});
66117
}
67118
};
68119

69120
return (
70-
<div className="relative flex flex-col flex-none border-r border-gray-200">
71-
<h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
72-
Config Overrides
73-
</h2>
74-
<Resizable
75-
minWidth={300}
76-
maxWidth={600}
77-
defaultSize={{width: 350, height: 'auto'}}
78-
enable={{right: true}}
79-
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
80-
<MonacoEditor
81-
path={'config.js'}
82-
language={'javascript'}
83-
value={store.config}
84-
onMount={handleMount}
85-
onChange={handleChange}
86-
options={{
87-
...monacoOptions,
88-
lineNumbers: 'off',
89-
folding: false,
90-
renderLineHighlight: 'none',
91-
scrollBeyondLastLine: false,
92-
hideCursorInOverviewRuler: true,
93-
overviewRulerBorder: false,
94-
overviewRulerLanes: 0,
95-
fontSize: 12,
96-
}}
97-
/>
98-
</Resizable>
121+
<div className="flex flex-row relative">
122+
{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+
</>
166+
) : (
167+
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
168+
<button
169+
title="Expand config editor"
170+
aria-label="Expand config editor"
171+
style={{
172+
transform: 'rotate(90deg) translate(-50%)',
173+
whiteSpace: 'nowrap',
174+
}}
175+
onClick={toggleExpanded}
176+
className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
177+
Config Overrides
178+
</button>
179+
</div>
180+
)}
99181
</div>
100182
);
101183
}

compiler/apps/playground/components/Editor/EditorImpl.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
import {transformFromAstSync} from '@babel/core';
4949
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
5050
import {useSearchParams} from 'next/navigation';
51-
import {parseAndFormatConfig} from '../../lib/configUtils';
5251

5352
function parseInput(
5453
input: string,
@@ -317,16 +316,11 @@ export default function Editor(): JSX.Element {
317316
mountStore = defaultStore;
318317
}
319318

320-
parseAndFormatConfig(mountStore.source).then(config => {
321-
dispatchStore({
322-
type: 'setStore',
323-
payload: {
324-
store: {
325-
...mountStore,
326-
config,
327-
},
328-
},
329-
});
319+
dispatchStore({
320+
type: 'setStore',
321+
payload: {
322+
store: mountStore,
323+
},
330324
});
331325
});
332326

compiler/apps/playground/lib/configUtils.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@
88
import parserBabel from 'prettier/plugins/babel';
99
import prettierPluginEstree from 'prettier/plugins/estree';
1010
import * as prettier from 'prettier/standalone';
11+
import {parsePluginOptions} from 'babel-plugin-react-compiler';
1112
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
1213

14+
export class ConfigError extends Error {
15+
constructor(message: string) {
16+
super(message);
17+
this.name = 'ConfigError';
18+
}
19+
}
1320
/**
1421
* Parse config from pragma and format it with prettier
1522
*/
1623
export async function parseAndFormatConfig(source: string): Promise<string> {
1724
const pragma = source.substring(0, source.indexOf('\n'));
1825
let configString = parseConfigPragmaAsString(pragma);
1926
if (configString !== '') {
20-
configString = `(${configString})`;
27+
configString = `\
28+
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
29+
30+
(${configString} satisfies Partial<PluginOptions>)`;
2131
}
2232

2333
try {
@@ -34,10 +44,10 @@ export async function parseAndFormatConfig(source: string): Promise<string> {
3444
}
3545

3646
function extractCurlyBracesContent(input: string): string {
37-
const startIndex = input.indexOf('{');
47+
const startIndex = input.indexOf('({') + 1;
3848
const endIndex = input.lastIndexOf('}');
3949
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
40-
throw new Error('No outer curly braces found in input');
50+
throw new Error('No outer curly braces found in input.');
4151
}
4252
return input.slice(startIndex, endIndex + 1);
4353
}
@@ -49,6 +59,27 @@ function cleanContent(content: string): string {
4959
.trim();
5060
}
5161

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+
5283
/**
5384
* Generate a the override pragma comment from a formatted config object string
5485
*/
@@ -58,6 +89,8 @@ export async function generateOverridePragmaFromConfig(
5889
const content = extractCurlyBracesContent(formattedConfigString);
5990
const cleanConfig = cleanContent(content);
6091

92+
validateConfigAsPluginOptions(cleanConfig);
93+
6194
// Format the config to ensure it's valid
6295
await prettier.format(`(${cleanConfig})`, {
6396
semi: false,

compiler/apps/playground/lib/defaultStore.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,31 @@ export default function MyApp() {
1313
}
1414
`;
1515

16+
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',
36+
} satisfies Partial<PluginOptions>);`;
37+
1638
export const defaultStore: Store = {
1739
source: index,
18-
config: '',
40+
config: defaultConfig,
1941
};
2042

2143
export const emptyStore: Store = {

compiler/apps/playground/lib/stores/store.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
compressToEncodedURIComponent,
1111
decompressFromEncodedURIComponent,
1212
} from 'lz-string';
13-
import {defaultStore} from '../defaultStore';
13+
import {defaultStore, defaultConfig} from '../defaultStore';
1414

1515
/**
1616
* Global Store for Playground
@@ -68,10 +68,10 @@ export function initStoreFromUrlOrLocalStorage(): Store {
6868
invariant(isValidStore(raw), 'Invalid Store');
6969

7070
// Add config property if missing for backwards compatibility
71-
if (!('config' in raw)) {
71+
if (!('config' in raw) || !raw['config']) {
7272
return {
7373
...raw,
74-
config: '',
74+
config: defaultConfig,
7575
};
7676
}
7777

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,6 @@ function parseConfigStringAsJS(
253253
});
254254
}
255255

256-
console.log('OVERRIDE:', parsedConfig);
257-
258256
const environment = parseConfigPragmaEnvironmentForTest(
259257
'',
260258
defaults.environment ?? {},

0 commit comments

Comments
 (0)