@samitouri / QOS-React-2 / commits / bcea86945c

[compiler][rfc] Enable more validations in playground. (#33777)

This is mostly to kick off conversation, i think we should go with a modified version of the implemented approach that i'll describe here. The playground currently serves two roles. The primary one we think about is for verifying compiler output. We use it for this sometimes, and developers frequently use it for this, including to send us repros if they have a potential bug. The second mode is to help developers learn about React. Part of that includes learning how to use React correctly — where it's helpful to see feedback about problematic code — and also to understand what kind of tools we provide compared to other frameworks, to make an informed choice about what tools they want to use. Currently we primarily think about the first role, but I think we should emphasize the second more. In this PR i'm doing the worst of both: enabling all the validations used by both the compiler and the linter by default. This means that code that would actually compile can fail with validations, which isn't great. What I think we should actually do is compile twice, one in "compilation" mode and once in "linter" mode, and combine the results as follows: * If "compilation" mode succeeds, show the compiled output _and_ any linter errors. * If "compilation" mode fails, show only the compilation mode failures. We should also distinguish which case it is when we show errors: "Compilation succeeded", "Compilation succeeded with linter errors", "Compilation failed". This lets developers continue to verify compiler output, while also turning the playground into a much more useful tool for learning React. Thoughts? --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33777). * #33981 * __->__ #33777

Joseph Savona committed Jul 24, 2025 at 15:52 UTC bcea86945cd5324f1a7f324a48fd2c7cb36e569b
3 files changed +99 -21
compiler/apps/playground/components/Editor/EditorImpl.tsx
+44 -11
@@ -142,7 +142,10 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
142 ],
143 ];
144
145 -function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
145 +function compile(
146 + source: string,
147 + mode: 'compiler' | 'linter',
148 +): [CompilerOutput, 'flow' | 'typescript'] {
149 const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
150 const error = new CompilerError();
151 const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
@@ -204,6 +207,22 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
207 };
208 const parsedOptions = parseConfigPragmaForTests(pragma, {
209 compilationMode: 'infer',
210 + environment:
211 + mode === 'linter'
212 + ? {
213 + // enabled in compiler
214 + validateRefAccessDuringRender: false,
215 + // enabled in linter
216 + validateNoSetStateInRender: true,
217 + validateNoSetStateInEffects: true,
218 + validateNoJSXInTryStatements: true,
219 + validateNoImpureFunctionsInRender: true,
220 + validateStaticComponents: true,
221 + validateNoFreezingKnownMutableFunctions: true,
222 + }
223 + : {
224 + /* use defaults for compiler mode */
225 + },
226 });
227 const opts: PluginOptions = parsePluginOptions({
228 ...parsedOptions,
@@ -249,9 +268,12 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
268 otherErrors.forEach(e => error.details.push(e));
269 }
270 if (error.hasErrors()) {
252 - return [{kind: 'err', results, error: error}, language];
271 + return [{kind: 'err', results, error}, language];
272 }
254 - return [{kind: 'ok', results, transformOutput}, language];
273 + return [
274 + {kind: 'ok', results, transformOutput, errors: error.details},
275 + language,
276 + ];
277 }
278
279 export default function Editor(): JSX.Element {
@@ -260,7 +282,11 @@ export default function Editor(): JSX.Element {
282 const dispatchStore = useStoreDispatch();
283 const {enqueueSnackbar} = useSnackbar();
284 const [compilerOutput, language] = useMemo(
263 - () => compile(deferredStore.source),
285 + () => compile(deferredStore.source, 'compiler'),
286 + [deferredStore.source],
287 + );
288 + const [linterOutput] = useMemo(
289 + () => compile(deferredStore.source, 'linter'),
290 [deferredStore.source],
291 );
292
@@ -286,19 +312,26 @@ export default function Editor(): JSX.Element {
312 });
313 });
314
315 + let mergedOutput: CompilerOutput;
316 + let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
317 + if (compilerOutput.kind === 'ok') {
318 + errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
319 + mergedOutput = {
320 + ...compilerOutput,
321 + errors,
322 + };
323 + } else {
324 + mergedOutput = compilerOutput;
325 + errors = compilerOutput.error.details;
326 + }
327 return (
328 <>
329 <div className="relative flex basis top-14">
330 <div className={clsx('relative sm:basis-1/4')}>
293 - <Input
294 - language={language}
295 - errors={
296 - compilerOutput.kind === 'err' ? compilerOutput.error.details : []
297 - }
298 - />
331 + <Input language={language} errors={errors} />
332 </div>
333 <div className={clsx('flex sm:flex flex-wrap')}>
301 - <Output store={deferredStore} compilerOutput={compilerOutput} />
334 + <Output store={deferredStore} compilerOutput={mergedOutput} />
335 </div>
336 </div>
337 </>
compiler/apps/playground/components/Editor/Output.tsx
+44 -8
@@ -11,7 +11,11 @@ import {
11 InformationCircleIcon,
12 } from '@heroicons/react/outline';
13 import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
14 -import {type CompilerError} from 'babel-plugin-react-compiler';
14 +import {
15 + CompilerErrorDetail,
16 + CompilerDiagnostic,
17 + type CompilerError,
18 +} from 'babel-plugin-react-compiler';
19 import parserBabel from 'prettier/plugins/babel';
20 import * as prettierPluginEstree from 'prettier/plugins/estree';
21 import * as prettier from 'prettier/standalone';
@@ -44,6 +48,7 @@ export type CompilerOutput =
48 kind: 'ok';
49 transformOutput: CompilerTransformOutput;
50 results: Map<string, Array<PrintedCompilerPipelineValue>>;
51 + errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
52 }
53 | {
54 kind: 'err';
@@ -123,10 +128,36 @@ async function tabify(
128 parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
129 plugins: [parserBabel, prettierPluginEstree],
130 });
131 +
132 + let output: string;
133 + let language: string;
134 + if (compilerOutput.errors.length === 0) {
135 + output = code;
136 + language = 'javascript';
137 + } else {
138 + language = 'markdown';
139 + output = `
140 +# Output
141 +
142 +React Compiler compiled this function sucessfully, but there are lint errors that indicate potential issues with the original code.
143 +
144 +## ${compilerOutput.errors.length} Lint Errors
145 +
146 +${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
147 +
148 +## Output
149 +
150 +\`\`\`js
151 +${code}
152 +\`\`\`
153 +`.trim();
154 + }
155 +
156 reorderedTabs.set(
127 - 'JS',
157 + 'Output',
158 <TextTabContent
129 - output={code}
159 + output={output}
160 + language={language}
161 diff={null}
162 showInfoPanel={false}></TextTabContent>,
163 );
@@ -147,9 +178,10 @@ async function tabify(
178 eslint: false,
179 });
180 reorderedTabs.set(
150 - 'Errors',
181 + 'Output',
182 <TextTabContent
183 output={errors}
184 + language="plaintext"
185 diff={null}
186 showInfoPanel={false}></TextTabContent>,
187 );
@@ -173,7 +205,9 @@ function getSourceMapUrl(code: string, map: string): string | null {
205 }
206
207 function Output({store, compilerOutput}: Props): JSX.Element {
176 - const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
208 + const [tabsOpen, setTabsOpen] = useState<Set<string>>(
209 + () => new Set(['Output']),
210 + );
211 const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
212 () => new Map(),
213 );
@@ -187,7 +221,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
221 );
222 if (compilerOutput.kind !== previousOutputKind) {
223 setPreviousOutputKind(compilerOutput.kind);
190 - setTabsOpen(new Set([compilerOutput.kind === 'ok' ? 'JS' : 'Errors']));
224 + setTabsOpen(new Set(['Output']));
225 }
226
227 useEffect(() => {
@@ -196,7 +230,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
230 });
231 }, [store.source, compilerOutput]);
232
199 - const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
233 + const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
234 let lastResult: string = '';
235 for (const [passName, results] of compilerOutput.results) {
236 for (const result of results) {
@@ -228,10 +262,12 @@ function TextTabContent({
262 output,
263 diff,
264 showInfoPanel,
265 + language,
266 }: {
267 output: string;
268 diff: string | null;
269 showInfoPanel: boolean;
270 + language: string;
271 }): JSX.Element {
272 const [diffMode, setDiffMode] = useState(false);
273 return (
@@ -282,7 +318,7 @@ function TextTabContent({
318 />
319 ) : (
320 <MonacoEditor
285 - defaultLanguage="javascript"
321 + language={language ?? 'javascript'}
322 value={output}
323 options={{
324 ...monacoOptions,
compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
+11 -2
@@ -113,8 +113,13 @@ function* splitPragma(
113 */
114 function parseConfigPragmaEnvironmentForTest(
115 pragma: string,
116 + defaultConfig: PartialEnvironmentConfig,
117 ): EnvironmentConfig {
117 - const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
118 + // throw early if the defaults are invalid
119 + EnvironmentConfigSchema.parse(defaultConfig);
120 +
121 + const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
122 + defaultConfig;
123
124 for (const {key, value: val} of splitPragma(pragma)) {
125 if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
@@ -174,9 +179,13 @@ export function parseConfigPragmaForTests(
179 pragma: string,
180 defaults: {
181 compilationMode: CompilationMode;
182 + environment?: PartialEnvironmentConfig;
183 },
184 ): PluginOptions {
179 - const environment = parseConfigPragmaEnvironmentForTest(pragma);
185 + const environment = parseConfigPragmaEnvironmentForTest(
186 + pragma,
187 + defaults.environment ?? {},
188 + );
189 const options: Record<keyof PluginOptions, unknown> = {
190 ...defaultOptions,
191 panicThreshold: 'all_errors',