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 a8b934160a0..c5d8c4cb6e8 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 afca5f9beb1..f2f1eb55351 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 47293204ac3..ce11f765302 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 00000000000..383eb7e71e6 --- /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 00000000000..c0a2769bb3f --- /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 17505024ffe..a904923b422 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 add42018a38..c70cd10ba53 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 d8744c3ca97..6cded7656b0 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 bf73c192c11..331aa66bcbe 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 582caebffb9..a5d8d17e242 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 {
-