From 8cae1d6728501f467e668ca6b14137ea9fb82c7e Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 26 Sep 2025 11:10:14 -0400 Subject: [PATCH 1/6] Fix hover order & applied configs panel remembers old values --- .../components/Editor/ConfigEditor.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index c70cd10ba53e8..7027e8f11f349 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -60,6 +60,7 @@ function ExpandedEditor({ const store = useStore(); const dispatchStore = useStoreDispatch(); const debounceTimerRef = useRef(null); + const lastValidOptionsRef = useRef(''); const handleChange: (value: string | undefined) => void = ( value: string | undefined, @@ -103,12 +104,16 @@ function ExpandedEditor({ }); }; - const formattedAppliedOptions = appliedOptions - ? prettyFormat(appliedOptions, { - printFunctionName: false, - printBasicPrototype: false, - }) - : 'Invalid configs'; + let formattedAppliedOptions = ''; + if (appliedOptions) { + formattedAppliedOptions = prettyFormat(appliedOptions, { + printFunctionName: false, + printBasicPrototype: false, + }); + lastValidOptionsRef.current = formattedAppliedOptions; + } else { + formattedAppliedOptions = lastValidOptionsRef.current; + } return ( -
+
-
+
Date: Fri, 26 Sep 2025 11:39:59 -0400 Subject: [PATCH 2/6] consolidate configs --- .../components/Editor/ConfigEditor.tsx | 22 +++---------------- .../components/Editor/monacoOptions.ts | 11 ++++++++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 7027e8f11f349..f1576d18ba5a7 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -12,7 +12,7 @@ import * as monaco from 'monaco-editor'; import React, {useState, useRef} from 'react'; import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; -import {monacoOptions} from './monacoOptions'; +import {monacoConfigOptions} from './monacoOptions'; import {IconChevron} from '../Icons/IconChevron'; import prettyFormat from 'pretty-format'; @@ -151,16 +151,7 @@ function ExpandedEditor({ onChange={handleChange} loading={''} className="monaco-editor-config" - options={{ - ...monacoOptions, - lineNumbers: 'off', - renderLineHighlight: 'none', - overviewRulerBorder: false, - overviewRulerLanes: 0, - fontSize: 12, - scrollBeyondLastLine: false, - glyphMargin: false, - }} + options={monacoConfigOptions} />
@@ -178,15 +169,8 @@ function ExpandedEditor({ loading={''} className="monaco-editor-applied-config" options={{ - ...monacoOptions, - lineNumbers: 'off', - renderLineHighlight: 'none', - overviewRulerBorder: false, - overviewRulerLanes: 0, - fontSize: 12, - scrollBeyondLastLine: false, + ...monacoConfigOptions, readOnly: true, - glyphMargin: false, }} />
diff --git a/compiler/apps/playground/components/Editor/monacoOptions.ts b/compiler/apps/playground/components/Editor/monacoOptions.ts index 7fed1b7875fce..d52c8bbedfa8d 100644 --- a/compiler/apps/playground/components/Editor/monacoOptions.ts +++ b/compiler/apps/playground/components/Editor/monacoOptions.ts @@ -32,3 +32,14 @@ export const monacoOptions: Partial = { tabSize: 2, }; + +export const monacoConfigOptions: Partial = { + ...monacoOptions, + lineNumbers: 'off', + renderLineHighlight: 'none', + overviewRulerBorder: false, + overviewRulerLanes: 0, + fontSize: 12, + scrollBeyondLastLine: false, + glyphMargin: false, +}; From 0dda9436fadb4559ae368841c506fe4c4c860d0a Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Tue, 30 Sep 2025 11:10:36 -0400 Subject: [PATCH 3/6] Remove ref on render, refactor code --- .../playground/__tests__/e2e/page.spec.ts | 12 +- .../components/Editor/ConfigEditor.tsx | 15 +- .../components/Editor/EditorImpl.tsx | 323 ++---------------- .../playground/components/StoreContext.tsx | 14 + compiler/apps/playground/lib/compilation.ts | 308 +++++++++++++++++ compiler/apps/playground/lib/defaultStore.ts | 2 + compiler/apps/playground/lib/stores/store.ts | 11 +- 7 files changed, 362 insertions(+), 323 deletions(-) create mode 100644 compiler/apps/playground/lib/compilation.ts diff --git a/compiler/apps/playground/__tests__/e2e/page.spec.ts b/compiler/apps/playground/__tests__/e2e/page.spec.ts index 4a10e8603bb28..733c77d029387 100644 --- a/compiler/apps/playground/__tests__/e2e/page.spec.ts +++ b/compiler/apps/playground/__tests__/e2e/page.spec.ts @@ -130,7 +130,7 @@ test('editor should open successfully', async ({page}) => { }); test('editor should compile from hash successfully', async ({page}) => { - const store: Store = { + const store: Partial = { source: TEST_SOURCE, config: defaultConfig, showInternals: false, @@ -153,7 +153,7 @@ test('editor should compile from hash successfully', async ({page}) => { }); test('reset button works', async ({page}) => { - const store: Store = { + const store: Partial = { source: TEST_SOURCE, config: defaultConfig, showInternals: false, @@ -234,7 +234,7 @@ test('show internals button toggles correctly', async ({page}) => { }); test('error is displayed when config has syntax error', async ({page}) => { - const store: Store = { + const store: Partial = { source: TEST_SOURCE, config: `compilationMode: `, showInternals: false, @@ -257,7 +257,7 @@ test('error is displayed when config has syntax error', async ({page}) => { }); test('error is displayed when config has validation error', async ({page}) => { - const store: Store = { + const store: Partial = { source: TEST_SOURCE, config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; @@ -285,7 +285,7 @@ test('error is displayed when config has validation error', async ({page}) => { test('disableMemoizationForDebugging flag works as expected', async ({ page, }) => { - const store: Store = { + const store: Partial = { source: TEST_SOURCE, config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; @@ -315,7 +315,7 @@ test('disableMemoizationForDebugging flag works as expected', async ({ TEST_CASE_INPUTS.forEach((t, idx) => test(`playground compiles: ${t.name}`, async ({page}) => { - const store: Store = { + const store: Partial = { source: t.input, config: defaultConfig, showInternals: false, diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index f1576d18ba5a7..7d98931732022 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -14,7 +14,6 @@ import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoConfigOptions} from './monacoOptions'; import {IconChevron} from '../Icons/IconChevron'; -import prettyFormat from 'pretty-format'; // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts'; @@ -60,7 +59,6 @@ function ExpandedEditor({ const store = useStore(); const dispatchStore = useStoreDispatch(); const debounceTimerRef = useRef(null); - const lastValidOptionsRef = useRef(''); const handleChange: (value: string | undefined) => void = ( value: string | undefined, @@ -104,17 +102,6 @@ function ExpandedEditor({ }); }; - let formattedAppliedOptions = ''; - if (appliedOptions) { - formattedAppliedOptions = prettyFormat(appliedOptions, { - printFunctionName: false, - printBasicPrototype: false, - }); - lastValidOptionsRef.current = formattedAppliedOptions; - } else { - formattedAppliedOptions = lastValidOptionsRef.current; - } - return ( { - // Extract the first line to quickly check for custom test directives - if (language === 'flow') { - return HermesParser.parse(input, { - babel: true, - flow: 'all', - sourceType: 'module', - enableExperimentalComponentSyntax: true, - }); - } else { - return babelParse(input, { - plugins: ['typescript', 'jsx'], - sourceType: 'module', - }) as ParseResult; - } -} - -function invokeCompiler( - source: string, - language: 'flow' | 'typescript', - options: PluginOptions, -): CompilerTransformOutput { - const ast = parseInput(source, language); - let result = transformFromAstSync(ast, source, { - filename: '_playgroundFile.js', - highlightCode: false, - retainLines: true, - plugins: [[BabelPluginReactCompiler, options]], - ast: true, - sourceType: 'module', - configFile: false, - sourceMaps: true, - babelrc: false, - }); - if (result?.ast == null || result?.code == null || result?.map == null) { - throw new Error('Expected successful compilation'); - } - return { - code: result.code, - sourceMaps: result.map, - language, - }; -} - -const COMMON_HOOKS: Array<[string, Hook]> = [ - [ - 'useFragment', - { - valueKind: ValueKind.Frozen, - effectKind: Effect.Freeze, - noAlias: true, - transitiveMixedData: true, - }, - ], - [ - 'usePaginationFragment', - { - valueKind: ValueKind.Frozen, - effectKind: Effect.Freeze, - noAlias: true, - transitiveMixedData: true, - }, - ], - [ - 'useRefetchableFragment', - { - valueKind: ValueKind.Frozen, - effectKind: Effect.Freeze, - noAlias: true, - transitiveMixedData: true, - }, - ], - [ - 'useLazyLoadQuery', - { - valueKind: ValueKind.Frozen, - effectKind: Effect.Freeze, - noAlias: true, - transitiveMixedData: true, - }, - ], - [ - 'usePreloadedQuery', - { - valueKind: ValueKind.Frozen, - effectKind: Effect.Freeze, - noAlias: true, - transitiveMixedData: true, - }, - ], -]; - -function parseOptions( - source: string, - mode: 'compiler' | 'linter', - configOverrides: string, -): PluginOptions { - // Extract the first line to quickly check for custom test directives - const pragma = source.substring(0, source.indexOf('\n')); - - const parsedPragmaOptions = parseConfigPragmaForTests(pragma, { - compilationMode: 'infer', - environment: - mode === 'linter' - ? { - // enabled in compiler - validateRefAccessDuringRender: false, - // enabled in linter - validateNoSetStateInRender: true, - validateNoSetStateInEffects: true, - validateNoJSXInTryStatements: true, - validateNoImpureFunctionsInRender: true, - validateStaticComponents: true, - validateNoFreezingKnownMutableFunctions: true, - validateNoVoidUseMemo: true, - } - : { - /* use defaults for compiler mode */ - }, - }); - - // Parse config overrides from config editor - let configOverrideOptions: any = {}; - const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s); - if (configOverrides.trim()) { - if (configMatch && configMatch[1]) { - const configString = configMatch[1].replace(/satisfies.*$/, '').trim(); - configOverrideOptions = new Function(`return (${configString})`)(); - } else { - throw new Error('Invalid override format'); - } - } - - const opts: PluginOptions = parsePluginOptions({ - ...parsedPragmaOptions, - ...configOverrideOptions, - environment: { - ...parsedPragmaOptions.environment, - ...configOverrideOptions.environment, - customHooks: new Map([...COMMON_HOOKS]), - }, - }); - - return opts; -} - -function compile( - source: string, - mode: 'compiler' | 'linter', - configOverrides: string, -): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] { - const results = new Map>(); - const error = new CompilerError(); - const otherErrors: Array = []; - const upsert: (result: PrintedCompilerPipelineValue) => void = result => { - const entry = results.get(result.name); - if (Array.isArray(entry)) { - entry.push(result); - } else { - results.set(result.name, [result]); - } - }; - let language: 'flow' | 'typescript'; - if (source.match(/\@flow/)) { - language = 'flow'; - } else { - language = 'typescript'; - } - let transformOutput; - - let baseOpts: PluginOptions | null = null; - try { - baseOpts = parseOptions(source, mode, configOverrides); - } catch (err) { - error.details.push( - new CompilerErrorDetail({ - category: ErrorCategory.Config, - reason: `Unexpected failure when transforming configs! \n${err}`, - loc: null, - suggestions: null, - }), - ); - } - if (baseOpts) { - try { - const logIR = (result: CompilerPipelineValue): void => { - switch (result.kind) { - case 'ast': { - break; - } - case 'hir': { - upsert({ - kind: 'hir', - fnName: result.value.id, - name: result.name, - value: printFunctionWithOutlined(result.value), - }); - break; - } - case 'reactive': { - upsert({ - kind: 'reactive', - fnName: result.value.id, - name: result.name, - value: printReactiveFunctionWithOutlined(result.value), - }); - break; - } - case 'debug': { - upsert({ - kind: 'debug', - fnName: null, - name: result.name, - value: result.value, - }); - break; - } - default: { - const _: never = result; - throw new Error(`Unhandled result ${result}`); - } - } - }; - // Add logger options to the parsed options - const opts = { - ...baseOpts, - logger: { - debugLogIRs: logIR, - logEvent: (_filename: string | null, event: LoggerEvent): void => { - if (event.kind === 'CompileError') { - otherErrors.push(event.detail); - } - }, - }, - }; - transformOutput = invokeCompiler(source, language, opts); - } catch (err) { - /** - * error might be an invariant violation or other runtime error - * (i.e. object shape that is not CompilerError) - */ - if (err instanceof CompilerError && err.details.length > 0) { - error.merge(err); - } else { - /** - * Handle unexpected failures by logging (to get a stack trace) - * and reporting - */ - error.details.push( - new CompilerErrorDetail({ - category: ErrorCategory.Invariant, - reason: `Unexpected failure when transforming input! \n${err}`, - loc: null, - suggestions: null, - }), - ); - } - } - } - // Only include logger errors if there weren't other errors - if (!error.hasErrors() && otherErrors.length !== 0) { - otherErrors.forEach(e => error.details.push(e)); - } - if (error.hasErrors()) { - return [{kind: 'err', results, error}, language, baseOpts]; - } - return [ - {kind: 'ok', results, transformOutput, errors: error.details}, - language, - baseOpts, - ]; -} +import {CompilerOutput, default as Output} from './Output'; +import {compile} from '../../lib/compilation'; +import prettyFormat from 'pretty-format'; export default function Editor(): JSX.Element { const store = useStore(); + const dispatchStore = useStoreDispatch(); const deferredStore = useDeferredValue(store); const [compilerOutput, language, appliedOptions] = useMemo( () => compile(deferredStore.source, 'compiler', deferredStore.config), @@ -336,6 +42,21 @@ export default function Editor(): JSX.Element { mergedOutput = compilerOutput; errors = compilerOutput.error.details; } + + useEffect(() => { + if (appliedOptions) { + dispatchStore({ + type: 'updateAppliedConfig', + payload: { + appliedConfig: prettyFormat(appliedOptions, { + printFunctionName: false, + printBasicPrototype: false, + }), + }, + }); + } + }, [appliedOptions]); + return ( <>
diff --git a/compiler/apps/playground/components/StoreContext.tsx b/compiler/apps/playground/components/StoreContext.tsx index 3f55678edf15d..829331b1aec3a 100644 --- a/compiler/apps/playground/components/StoreContext.tsx +++ b/compiler/apps/playground/components/StoreContext.tsx @@ -82,6 +82,12 @@ type ReducerAction = config: string; }; } + | { + type: 'updateAppliedConfig'; + payload: { + appliedConfig: string; + }; + } | { type: 'toggleInternals'; }; @@ -108,6 +114,14 @@ function storeReducer(store: Store, action: ReducerAction): Store { }; return newStore; } + case 'updateAppliedConfig': { + const appliedConfig = action.payload.appliedConfig; + const newStore = { + ...store, + appliedConfig, + }; + return newStore; + } case 'toggleInternals': { const newStore = { ...store, diff --git a/compiler/apps/playground/lib/compilation.ts b/compiler/apps/playground/lib/compilation.ts new file mode 100644 index 0000000000000..10bf0164c0e77 --- /dev/null +++ b/compiler/apps/playground/lib/compilation.ts @@ -0,0 +1,308 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {parse as babelParse, ParseResult} from '@babel/parser'; +import * as HermesParser from 'hermes-parser'; +import * as t from '@babel/types'; +import BabelPluginReactCompiler, { + CompilerError, + CompilerErrorDetail, + CompilerDiagnostic, + Effect, + ErrorCategory, + parseConfigPragmaForTests, + ValueKind, + type Hook, + PluginOptions, + CompilerPipelineValue, + parsePluginOptions, + printReactiveFunctionWithOutlined, + printFunctionWithOutlined, + type LoggerEvent, +} from 'babel-plugin-react-compiler'; +import {transformFromAstSync} from '@babel/core'; +import type { + CompilerOutput, + CompilerTransformOutput, + PrintedCompilerPipelineValue, +} from '../components/Editor/Output'; + +function parseInput( + input: string, + language: 'flow' | 'typescript', +): ParseResult { + // Extract the first line to quickly check for custom test directives + if (language === 'flow') { + return HermesParser.parse(input, { + babel: true, + flow: 'all', + sourceType: 'module', + enableExperimentalComponentSyntax: true, + }); + } else { + return babelParse(input, { + plugins: ['typescript', 'jsx'], + sourceType: 'module', + }) as ParseResult; + } +} + +function invokeCompiler( + source: string, + language: 'flow' | 'typescript', + options: PluginOptions, +): CompilerTransformOutput { + const ast = parseInput(source, language); + let result = transformFromAstSync(ast, source, { + filename: '_playgroundFile.js', + highlightCode: false, + retainLines: true, + plugins: [[BabelPluginReactCompiler, options]], + ast: true, + sourceType: 'module', + configFile: false, + sourceMaps: true, + babelrc: false, + }); + if (result?.ast == null || result?.code == null || result?.map == null) { + throw new Error('Expected successful compilation'); + } + return { + code: result.code, + sourceMaps: result.map, + language, + }; +} + +const COMMON_HOOKS: Array<[string, Hook]> = [ + [ + 'useFragment', + { + valueKind: ValueKind.Frozen, + effectKind: Effect.Freeze, + noAlias: true, + transitiveMixedData: true, + }, + ], + [ + 'usePaginationFragment', + { + valueKind: ValueKind.Frozen, + effectKind: Effect.Freeze, + noAlias: true, + transitiveMixedData: true, + }, + ], + [ + 'useRefetchableFragment', + { + valueKind: ValueKind.Frozen, + effectKind: Effect.Freeze, + noAlias: true, + transitiveMixedData: true, + }, + ], + [ + 'useLazyLoadQuery', + { + valueKind: ValueKind.Frozen, + effectKind: Effect.Freeze, + noAlias: true, + transitiveMixedData: true, + }, + ], + [ + 'usePreloadedQuery', + { + valueKind: ValueKind.Frozen, + effectKind: Effect.Freeze, + noAlias: true, + transitiveMixedData: true, + }, + ], +]; + +function parseOptions( + source: string, + mode: 'compiler' | 'linter', + configOverrides: string, +): PluginOptions { + // Extract the first line to quickly check for custom test directives + const pragma = source.substring(0, source.indexOf('\n')); + + const parsedPragmaOptions = parseConfigPragmaForTests(pragma, { + compilationMode: 'infer', + environment: + mode === 'linter' + ? { + // enabled in compiler + validateRefAccessDuringRender: false, + // enabled in linter + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + } + : { + /* use defaults for compiler mode */ + }, + }); + + // Parse config overrides from config editor + let configOverrideOptions: any = {}; + const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s); + if (configOverrides.trim()) { + if (configMatch && configMatch[1]) { + const configString = configMatch[1].replace(/satisfies.*$/, '').trim(); + configOverrideOptions = new Function(`return (${configString})`)(); + } else { + throw new Error('Invalid override format'); + } + } + + const opts: PluginOptions = parsePluginOptions({ + ...parsedPragmaOptions, + ...configOverrideOptions, + environment: { + ...parsedPragmaOptions.environment, + ...configOverrideOptions.environment, + customHooks: new Map([...COMMON_HOOKS]), + }, + }); + + return opts; +} + +export function compile( + source: string, + mode: 'compiler' | 'linter', + configOverrides: string, +): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] { + const results = new Map>(); + const error = new CompilerError(); + const otherErrors: Array = []; + const upsert: (result: PrintedCompilerPipelineValue) => void = result => { + const entry = results.get(result.name); + if (Array.isArray(entry)) { + entry.push(result); + } else { + results.set(result.name, [result]); + } + }; + let language: 'flow' | 'typescript'; + if (source.match(/\@flow/)) { + language = 'flow'; + } else { + language = 'typescript'; + } + let transformOutput; + + let baseOpts: PluginOptions | null = null; + try { + baseOpts = parseOptions(source, mode, configOverrides); + } catch (err) { + error.details.push( + new CompilerErrorDetail({ + category: ErrorCategory.Config, + reason: `Unexpected failure when transforming configs! \n${err}`, + loc: null, + suggestions: null, + }), + ); + } + if (baseOpts) { + try { + const logIR = (result: CompilerPipelineValue): void => { + switch (result.kind) { + case 'ast': { + break; + } + case 'hir': { + upsert({ + kind: 'hir', + fnName: result.value.id, + name: result.name, + value: printFunctionWithOutlined(result.value), + }); + break; + } + case 'reactive': { + upsert({ + kind: 'reactive', + fnName: result.value.id, + name: result.name, + value: printReactiveFunctionWithOutlined(result.value), + }); + break; + } + case 'debug': { + upsert({ + kind: 'debug', + fnName: null, + name: result.name, + value: result.value, + }); + break; + } + default: { + const _: never = result; + throw new Error(`Unhandled result ${result}`); + } + } + }; + // Add logger options to the parsed options + const opts = { + ...baseOpts, + logger: { + debugLogIRs: logIR, + logEvent: (_filename: string | null, event: LoggerEvent): void => { + if (event.kind === 'CompileError') { + otherErrors.push(event.detail); + } + }, + }, + }; + transformOutput = invokeCompiler(source, language, opts); + } catch (err) { + /** + * error might be an invariant violation or other runtime error + * (i.e. object shape that is not CompilerError) + */ + if (err instanceof CompilerError && err.details.length > 0) { + error.merge(err); + } else { + /** + * Handle unexpected failures by logging (to get a stack trace) + * and reporting + */ + error.details.push( + new CompilerErrorDetail({ + category: ErrorCategory.Invariant, + reason: `Unexpected failure when transforming input! \n${err}`, + loc: null, + suggestions: null, + }), + ); + } + } + } + // Only include logger errors if there weren't other errors + if (!error.hasErrors() && otherErrors.length !== 0) { + otherErrors.forEach(e => error.details.push(e)); + } + if (error.hasErrors()) { + return [{kind: 'err', results, error}, language, baseOpts]; + } + return [ + {kind: 'ok', results, transformOutput, errors: error.details}, + language, + baseOpts, + ]; +} diff --git a/compiler/apps/playground/lib/defaultStore.ts b/compiler/apps/playground/lib/defaultStore.ts index 2baada0b8179a..cec7f7a50f4a9 100644 --- a/compiler/apps/playground/lib/defaultStore.ts +++ b/compiler/apps/playground/lib/defaultStore.ts @@ -24,10 +24,12 @@ export const defaultStore: Store = { source: index, config: defaultConfig, showInternals: false, + appliedConfig: '', }; export const emptyStore: Store = { source: '', config: '', showInternals: false, + appliedConfig: '', }; diff --git a/compiler/apps/playground/lib/stores/store.ts b/compiler/apps/playground/lib/stores/store.ts index 6655efa274089..112e9013478d5 100644 --- a/compiler/apps/playground/lib/stores/store.ts +++ b/compiler/apps/playground/lib/stores/store.ts @@ -19,8 +19,9 @@ export interface Store { source: string; config: string; showInternals: boolean; + appliedConfig: string; } -export function encodeStore(store: Store): string { +export function encodeStore(store: Partial): string { return compressToEncodedURIComponent(JSON.stringify(store)); } export function decodeStore(hash: string): any { @@ -31,7 +32,12 @@ export function decodeStore(hash: string): any { * Serialize, encode, and save @param store to localStorage and update URL. */ export function saveStore(store: Store): void { - const hash = encodeStore(store); + const partialStore = { + source: store.source, + config: store.config, + showInternals: store.showInternals, + }; + const hash = encodeStore(partialStore); localStorage.setItem('playgroundStore', hash); history.replaceState({}, '', `#${hash}`); } @@ -73,5 +79,6 @@ export function initStoreFromUrlOrLocalStorage(): Store { source: raw.source, config: 'config' in raw && raw['config'] ? raw.config : defaultConfig, showInternals: 'showInternals' in raw ? raw.showInternals : false, + appliedConfig: '', }; } From c59ef9a72451d3313be763dd28ee76385f2ef431 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Thu, 2 Oct 2025 18:38:40 -0400 Subject: [PATCH 4/6] Remove useEffect --- .../components/Editor/ConfigEditor.tsx | 12 ++++----- .../components/Editor/EditorImpl.tsx | 25 ++++++++----------- compiler/apps/playground/lib/defaultStore.ts | 2 -- compiler/apps/playground/lib/stores/store.ts | 11 ++------ 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 7d98931732022..e7f38f998be3d 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -21,9 +21,9 @@ import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts'; loader.config({monaco}); export default function ConfigEditor({ - appliedOptions, + formattedAppliedConfig, }: { - appliedOptions: PluginOptions | null; + formattedAppliedConfig: string; }): React.ReactElement { const [isExpanded, setIsExpanded] = useState(false); @@ -36,7 +36,7 @@ export default function ConfigEditor({ }}>
void; - appliedOptions: PluginOptions | null; + formattedAppliedConfig: string; }): React.ReactElement { const store = useStore(); const dispatchStore = useStoreDispatch(); @@ -152,7 +152,7 @@ function ExpandedEditor({ compile(deferredStore.source, 'linter', deferredStore.config), [deferredStore.source, deferredStore.config], ); + const [formattedAppliedConfig, setFormattedAppliedConfig] = useState(''); let mergedOutput: CompilerOutput; let errors: Array; @@ -43,25 +44,21 @@ export default function Editor(): JSX.Element { errors = compilerOutput.error.details; } - useEffect(() => { - if (appliedOptions) { - dispatchStore({ - type: 'updateAppliedConfig', - payload: { - appliedConfig: prettyFormat(appliedOptions, { - printFunctionName: false, - printBasicPrototype: false, - }), - }, - }); + if (appliedOptions) { + const formatted = prettyFormat(appliedOptions, { + printFunctionName: false, + printBasicPrototype: false, + }); + if (formatted !== formattedAppliedConfig) { + setFormattedAppliedConfig(formatted); } - }, [appliedOptions]); + } return ( <>
- +
diff --git a/compiler/apps/playground/lib/defaultStore.ts b/compiler/apps/playground/lib/defaultStore.ts index cec7f7a50f4a9..2baada0b8179a 100644 --- a/compiler/apps/playground/lib/defaultStore.ts +++ b/compiler/apps/playground/lib/defaultStore.ts @@ -24,12 +24,10 @@ export const defaultStore: Store = { source: index, config: defaultConfig, showInternals: false, - appliedConfig: '', }; export const emptyStore: Store = { source: '', config: '', showInternals: false, - appliedConfig: '', }; diff --git a/compiler/apps/playground/lib/stores/store.ts b/compiler/apps/playground/lib/stores/store.ts index 112e9013478d5..6655efa274089 100644 --- a/compiler/apps/playground/lib/stores/store.ts +++ b/compiler/apps/playground/lib/stores/store.ts @@ -19,9 +19,8 @@ export interface Store { source: string; config: string; showInternals: boolean; - appliedConfig: string; } -export function encodeStore(store: Partial): string { +export function encodeStore(store: Store): string { return compressToEncodedURIComponent(JSON.stringify(store)); } export function decodeStore(hash: string): any { @@ -32,12 +31,7 @@ export function decodeStore(hash: string): any { * Serialize, encode, and save @param store to localStorage and update URL. */ export function saveStore(store: Store): void { - const partialStore = { - source: store.source, - config: store.config, - showInternals: store.showInternals, - }; - const hash = encodeStore(partialStore); + const hash = encodeStore(store); localStorage.setItem('playgroundStore', hash); history.replaceState({}, '', `#${hash}`); } @@ -79,6 +73,5 @@ export function initStoreFromUrlOrLocalStorage(): Store { source: raw.source, config: 'config' in raw && raw['config'] ? raw.config : defaultConfig, showInternals: 'showInternals' in raw ? raw.showInternals : false, - appliedConfig: '', }; } From c6337ca25d8bcb5edb3a03bb78a05acad7137456 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Thu, 2 Oct 2025 18:40:28 -0400 Subject: [PATCH 5/6] revert store changes --- .../apps/playground/__tests__/e2e/page.spec.ts | 12 ++++++------ .../apps/playground/components/StoreContext.tsx | 14 -------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/compiler/apps/playground/__tests__/e2e/page.spec.ts b/compiler/apps/playground/__tests__/e2e/page.spec.ts index 733c77d029387..4a10e8603bb28 100644 --- a/compiler/apps/playground/__tests__/e2e/page.spec.ts +++ b/compiler/apps/playground/__tests__/e2e/page.spec.ts @@ -130,7 +130,7 @@ test('editor should open successfully', async ({page}) => { }); test('editor should compile from hash successfully', async ({page}) => { - const store: Partial = { + const store: Store = { source: TEST_SOURCE, config: defaultConfig, showInternals: false, @@ -153,7 +153,7 @@ test('editor should compile from hash successfully', async ({page}) => { }); test('reset button works', async ({page}) => { - const store: Partial = { + const store: Store = { source: TEST_SOURCE, config: defaultConfig, showInternals: false, @@ -234,7 +234,7 @@ test('show internals button toggles correctly', async ({page}) => { }); test('error is displayed when config has syntax error', async ({page}) => { - const store: Partial = { + const store: Store = { source: TEST_SOURCE, config: `compilationMode: `, showInternals: false, @@ -257,7 +257,7 @@ test('error is displayed when config has syntax error', async ({page}) => { }); test('error is displayed when config has validation error', async ({page}) => { - const store: Partial = { + const store: Store = { source: TEST_SOURCE, config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; @@ -285,7 +285,7 @@ test('error is displayed when config has validation error', async ({page}) => { test('disableMemoizationForDebugging flag works as expected', async ({ page, }) => { - const store: Partial = { + const store: Store = { source: TEST_SOURCE, config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; @@ -315,7 +315,7 @@ test('disableMemoizationForDebugging flag works as expected', async ({ TEST_CASE_INPUTS.forEach((t, idx) => test(`playground compiles: ${t.name}`, async ({page}) => { - const store: Partial = { + const store: Store = { source: t.input, config: defaultConfig, showInternals: false, diff --git a/compiler/apps/playground/components/StoreContext.tsx b/compiler/apps/playground/components/StoreContext.tsx index 829331b1aec3a..3f55678edf15d 100644 --- a/compiler/apps/playground/components/StoreContext.tsx +++ b/compiler/apps/playground/components/StoreContext.tsx @@ -82,12 +82,6 @@ type ReducerAction = config: string; }; } - | { - type: 'updateAppliedConfig'; - payload: { - appliedConfig: string; - }; - } | { type: 'toggleInternals'; }; @@ -114,14 +108,6 @@ function storeReducer(store: Store, action: ReducerAction): Store { }; return newStore; } - case 'updateAppliedConfig': { - const appliedConfig = action.payload.appliedConfig; - const newStore = { - ...store, - appliedConfig, - }; - return newStore; - } case 'toggleInternals': { const newStore = { ...store, From 818ab7d58a8ced62934933b0a46054887d517b99 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 3 Oct 2025 10:38:11 -0400 Subject: [PATCH 6/6] lint fix --- compiler/apps/playground/components/Editor/ConfigEditor.tsx | 1 - compiler/apps/playground/components/Editor/EditorImpl.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index b79f7d2e57e2b..18f904d225f0e 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -6,7 +6,6 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {PluginOptions} from 'babel-plugin-react-compiler'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; import React, { diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 15bcbf335ced0..5b39b91654e27 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -10,7 +10,7 @@ import { CompilerDiagnostic, } from 'babel-plugin-react-compiler'; import {useDeferredValue, useMemo, useState} from 'react'; -import {useStore, useStoreDispatch} from '../StoreContext'; +import {useStore} from '../StoreContext'; import ConfigEditor from './ConfigEditor'; import Input from './Input'; import {CompilerOutput, default as Output} from './Output'; @@ -19,7 +19,6 @@ import prettyFormat from 'pretty-format'; export default function Editor(): JSX.Element { const store = useStore(); - const dispatchStore = useStoreDispatch(); const deferredStore = useDeferredValue(store); const [compilerOutput, language, appliedOptions] = useMemo( () => compile(deferredStore.source, 'compiler', deferredStore.config),