From 07e4974bad015a44c2da488c80a494318007e58d Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 22 Sep 2025 16:51:57 +0200 Subject: [PATCH 1/4] [compiler] Don't leak global `__DEV__` type (#34551) --- compiler/packages/babel-plugin-react-compiler/src/index.ts | 1 + compiler/packages/babel-plugin-react-compiler/tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index a8b934160a023..c5d8c4cb6e86e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -51,6 +51,7 @@ export { } from './ReactiveScopes'; export {parseConfigPragmaForTests} from './Utils/TestUtils'; declare global { + // @internal let __DEV__: boolean | null | undefined; } diff --git a/compiler/packages/babel-plugin-react-compiler/tsconfig.json b/compiler/packages/babel-plugin-react-compiler/tsconfig.json index afca5f9beb13c..f2f1eb55351c0 100644 --- a/compiler/packages/babel-plugin-react-compiler/tsconfig.json +++ b/compiler/packages/babel-plugin-react-compiler/tsconfig.json @@ -10,6 +10,7 @@ "importsNotUsedAsValues": "remove", "noUncheckedIndexedAccess": false, "noUnusedParameters": false, + "stripInternal": true, "useUnknownInCatchVariables": true, "target": "ES2015", // ideally turn off only during dev, or on a per-file basis From cd85bb561612f9da467a0cb1978afbf843d25757 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 22 Sep 2025 17:09:50 +0200 Subject: [PATCH 2/4] Include Fizz runtime diff in CI (#34525) --- .github/workflows/runtime_build_and_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index 47293204ac390..ce11f76530243 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -194,7 +194,7 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: | yarn generate-inline-fizz-runtime - git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false) + git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false) # ----- FEATURE FLAGS ----- flags: @@ -567,7 +567,7 @@ jobs: - name: Search build artifacts for unminified errors run: | yarn extract-errors - git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) + git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) check_release_dependencies: name: Check release dependencies From 1eca9a2747bfbe9832c3ef1665642e12d7d87d1e Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Mon, 22 Sep 2025 12:11:45 -0400 Subject: [PATCH 3/4] [playground] Add compiler playground tests (#34528) ## Summary Added more tests for the compiler playground with the addition of the new config editor and "Show Internals" button. Added testing to check for incomplete store params in the URL, toggle functionality, and correct errors showing for syntax/validation errors in the config overrides. --- .../page.spec.ts/default-config.txt | 5 + .../disableMemoizationForDebugging-output.txt | 14 ++ .../playground/__tests__/e2e/page.spec.ts | 172 ++++++++++++++++-- .../components/Editor/ConfigEditor.tsx | 4 +- .../playground/components/Editor/Input.tsx | 1 + .../playground/components/Editor/Output.tsx | 1 + .../apps/playground/components/Header.tsx | 2 +- 7 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/default-config.txt create mode 100644 compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/disableMemoizationForDebugging-output.txt diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/default-config.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/default-config.txt new file mode 100644 index 0000000000000..383eb7e71e67f --- /dev/null +++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/default-config.txt @@ -0,0 +1,5 @@ +import type { PluginOptions } from  +'babel-plugin-react-compiler/dist'; +({ +  //compilationMode: "all" +} satisfies Partial); \ No newline at end of file diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/disableMemoizationForDebugging-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/disableMemoizationForDebugging-output.txt new file mode 100644 index 0000000000000..c0a2769bb3fad --- /dev/null +++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/disableMemoizationForDebugging-output.txt @@ -0,0 +1,14 @@ +import { c as _c } from "react/compiler-runtime"; +export default function TestComponent(t0) { + const $ = _c(2); + const { x } = t0; + let t1; + if ($[0] !== x || true) { + t1 = ; + $[0] = x; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} diff --git a/compiler/apps/playground/__tests__/e2e/page.spec.ts b/compiler/apps/playground/__tests__/e2e/page.spec.ts index 17505024ffeac..a904923b4228b 100644 --- a/compiler/apps/playground/__tests__/e2e/page.spec.ts +++ b/compiler/apps/playground/__tests__/e2e/page.spec.ts @@ -5,8 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import {expect, test} from '@playwright/test'; +import {expect, test, type Page} from '@playwright/test'; import {encodeStore, type Store} from '../../lib/stores'; +import {defaultConfig} from '../../lib/defaultStore'; import {format} from 'prettier'; function isMonacoLoaded(): boolean { @@ -20,6 +21,15 @@ function formatPrint(data: Array): Promise { return format(data.join(''), {parser: 'babel'}); } +async function expandConfigs(page: Page): Promise { + const expandButton = page.locator('[title="Expand config editor"]'); + expandButton.click(); +} + +const TEST_SOURCE = `export default function TestComponent({ x }) { + return ; +}`; + const TEST_CASE_INPUTS = [ { name: 'module-scope-use-memo', @@ -121,10 +131,9 @@ test('editor should open successfully', async ({page}) => { test('editor should compile from hash successfully', async ({page}) => { const store: Store = { - source: `export default function TestComponent({ x }) { - return ; - } - `, + source: TEST_SOURCE, + config: defaultConfig, + showInternals: false, }; const hash = encodeStore(store); await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); @@ -136,7 +145,7 @@ test('editor should compile from hash successfully', async ({page}) => { path: 'test-results/01-compiles-from-hash.png', }); const text = - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? []; + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; const output = await formatPrint(text); expect(output).not.toEqual(''); @@ -145,10 +154,9 @@ test('editor should compile from hash successfully', async ({page}) => { test('reset button works', async ({page}) => { const store: Store = { - source: `export default function TestComponent({ x }) { - return ; - } - `, + source: TEST_SOURCE, + config: defaultConfig, + showInternals: false, }; const hash = encodeStore(store); await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); @@ -157,33 +165,171 @@ test('reset button works', async ({page}) => { // Reset button works page.on('dialog', dialog => dialog.accept()); await page.getByRole('button', {name: 'Reset'}).click(); + await expandConfigs(page); + await page.screenshot({ fullPage: true, path: 'test-results/02-reset-button-works.png', }); const text = - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? []; + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; const output = await formatPrint(text); + const configText = + (await page.locator('.monaco-editor-config').allInnerTexts()) ?? []; + const configOutput = configText.join(''); + expect(output).not.toEqual(''); expect(output).toMatchSnapshot('02-default-output.txt'); + expect(configOutput).not.toEqual(''); + expect(configOutput).toMatchSnapshot('default-config.txt'); +}); + +test('defaults load when only source is in Store', async ({page}) => { + // Test for backwards compatibility + const partial = { + source: TEST_SOURCE, + }; + const hash = encodeStore(partial as Store); + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); + await page.waitForFunction(isMonacoLoaded); + await expandConfigs(page); + + await page.screenshot({ + fullPage: true, + path: 'test-results/03-missing-defaults.png', + }); + + // Config editor has default config + const configText = + (await page.locator('.monaco-editor-config').allInnerTexts()) ?? []; + const configOutput = configText.join(''); + + expect(configOutput).not.toEqual(''); + expect(configOutput).toMatchSnapshot('default-config.txt'); + + const checkbox = page.locator('label.show-internals'); + await expect(checkbox).not.toBeChecked(); + const ssaTab = page.locator('text=SSA'); + await expect(ssaTab).not.toBeVisible(); +}); + +test('show internals button toggles correctly', async ({page}) => { + await page.goto(`/`, {waitUntil: 'networkidle'}); + await page.waitForFunction(isMonacoLoaded); + + // show internals should be off + const checkbox = page.locator('label.show-internals'); + await checkbox.click(); + + await page.screenshot({ + fullPage: true, + path: 'test-results/04-show-internals-on.png', + }); + + await expect(checkbox).toBeChecked(); + + const ssaTab = page.locator('text=SSA'); + await expect(ssaTab).toBeVisible(); +}); + +test('error is displayed when config has syntax error', async ({page}) => { + const store: Store = { + source: TEST_SOURCE, + config: `compilationMode: `, + showInternals: false, + }; + const hash = encodeStore(store); + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); + await page.waitForFunction(isMonacoLoaded); + await expandConfigs(page); + await page.screenshot({ + fullPage: true, + path: 'test-results/05-config-syntax-error.png', + }); + + const text = + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; + const output = text.join(''); + + // Remove hidden chars + expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format'); +}); + +test('error is displayed when config has validation error', async ({page}) => { + const store: Store = { + source: TEST_SOURCE, + config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; + +({ + compilationMode: "123" +} satisfies Partial);`, + showInternals: false, + }; + const hash = encodeStore(store); + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); + await page.waitForFunction(isMonacoLoaded); + await expandConfigs(page); + await page.screenshot({ + fullPage: true, + path: 'test-results/06-config-validation-error.png', + }); + + const text = + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; + const output = text.join(''); + + expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode'); +}); + +test('disableMemoizationForDebugging flag works as expected', async ({ + page, +}) => { + const store: Store = { + source: TEST_SOURCE, + config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist'; + +({ + environment: { + disableMemoizationForDebugging: true + } +} satisfies Partial);`, + showInternals: false, + }; + const hash = encodeStore(store); + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); + await page.waitForFunction(isMonacoLoaded); + await expandConfigs(page); + await page.screenshot({ + fullPage: true, + path: 'test-results/07-config-disableMemoizationForDebugging-flag.png', + }); + + const text = + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; + const output = await formatPrint(text); + + expect(output).not.toEqual(''); + expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt'); }); TEST_CASE_INPUTS.forEach((t, idx) => test(`playground compiles: ${t.name}`, async ({page}) => { const store: Store = { source: t.input, + config: defaultConfig, + showInternals: false, }; const hash = encodeStore(store); await page.goto(`/#${hash}`, {waitUntil: 'networkidle'}); await page.waitForFunction(isMonacoLoaded); await page.screenshot({ fullPage: true, - path: `test-results/03-0${idx}-${t.name}.png`, + path: `test-results/08-0${idx}-${t.name}.png`, }); const text = - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? []; + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? []; let output: string; if (t.noFormat) { output = text.join(''); diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index add42018a3879..c70cd10ba53e8 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -9,7 +9,7 @@ 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, {useState, useRef, useEffect} from 'react'; +import React, {useState, useRef} from 'react'; import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; @@ -145,6 +145,7 @@ function ExpandedEditor({ onMount={handleMount} onChange={handleChange} loading={''} + className="monaco-editor-config" options={{ ...monacoOptions, lineNumbers: 'off', @@ -170,6 +171,7 @@ function ExpandedEditor({ language={'javascript'} value={formattedAppliedOptions} loading={''} + className="monaco-editor-applied-config" options={{ ...monacoOptions, lineNumbers: 'off', diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index d8744c3ca9770..6cded7656b06f 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -145,6 +145,7 @@ export default function Input({errors, language}: Props): JSX.Element { value={store.source} onMount={handleMount} onChange={handleChange} + className="monaco-editor-input" options={monacoOptions} loading={''} /> diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index bf73c192c1152..331aa66bcbeb4 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -340,6 +340,7 @@ function TextTabContent({ language={language ?? 'javascript'} value={output} loading={''} + className="monaco-editor-output" options={{ ...monacoOptions, readOnly: true, diff --git a/compiler/apps/playground/components/Header.tsx b/compiler/apps/playground/components/Header.tsx index 582caebffb9c3..a5d8d17e2426c 100644 --- a/compiler/apps/playground/components/Header.tsx +++ b/compiler/apps/playground/components/Header.tsx @@ -58,7 +58,7 @@ export default function Header(): JSX.Element {
-