From 3e42fd223865a74c082267151b7a0ab4df7ceff8 Mon Sep 17 00:00:00 2001 From: Jack Hanford Date: Wed, 9 Sep 2026 08:46:45 -0700 Subject: [PATCH 1/4] feat(react)!: migrate to Oxc and expose React Compiler --- .changeset/friendly-tacos-compile.md | 17 + .../fixtures/react-compiler/astro.config.mjs | 4 + .../e2e/fixtures/react-compiler/package.json | 13 + .../react-compiler/src/components/Counter.jsx | 6 + .../react-compiler/src/pages/index.astro | 9 + packages/astro/e2e/react-compiler.test.ts | 47 +++ packages/integrations/react/README.md | 23 ++ packages/integrations/react/package.json | 13 +- packages/integrations/react/src/index.ts | 41 ++- .../react-compiler-disabled/astro.config.mjs | 1 + .../react-compiler-disabled/package.json | 13 + .../src/components/Counter.jsx | 6 + .../src/pages/index.astro | 4 + .../fixtures/react-compiler/astro.config.mjs | 1 + .../test/fixtures/react-compiler/package.json | 13 + .../react-compiler/src/components/Counter.jsx | 6 + .../react-compiler/src/pages/index.astro | 4 + .../react/test/react-compiler.test.ts | 59 +++ pnpm-lock.yaml | 346 +++++++++++++++--- 19 files changed, 561 insertions(+), 65 deletions(-) create mode 100644 .changeset/friendly-tacos-compile.md create mode 100644 packages/astro/e2e/fixtures/react-compiler/astro.config.mjs create mode 100644 packages/astro/e2e/fixtures/react-compiler/package.json create mode 100644 packages/astro/e2e/fixtures/react-compiler/src/components/Counter.jsx create mode 100644 packages/astro/e2e/fixtures/react-compiler/src/pages/index.astro create mode 100644 packages/astro/e2e/react-compiler.test.ts create mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/astro.config.mjs create mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/package.json create mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx create mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro create mode 100644 packages/integrations/react/test/fixtures/react-compiler/astro.config.mjs create mode 100644 packages/integrations/react/test/fixtures/react-compiler/package.json create mode 100644 packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx create mode 100644 packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro create mode 100644 packages/integrations/react/test/react-compiler.test.ts diff --git a/.changeset/friendly-tacos-compile.md b/.changeset/friendly-tacos-compile.md new file mode 100644 index 000000000000..b5648ba00ece --- /dev/null +++ b/.changeset/friendly-tacos-compile.md @@ -0,0 +1,17 @@ +--- +'@astrojs/react': major +--- + +Migrates the React integration to `@vitejs/plugin-react` v6, using Oxc for JSX and Fast Refresh. Removes the `babel` integration option; projects with custom Babel transforms must configure `@rolldown/plugin-babel` under `vite.plugins` instead. + +Adds an opt-in `compiler` option for the Oxc-based React Compiler. Install `oxc-transform-react` and enable it in your Astro config: + +```js +import react from '@astrojs/react'; + +export default { + integrations: [react({ compiler: true })], +}; +``` + +The compiler memoizes client components and hooks. Server rendering is not compiled. The target defaults to the installed React major version; React 17 and 18 projects also need `react-compiler-runtime` installed. Pass an options object to configure the compiler, for example `compiler: { compilationMode: 'annotation' }`. The integration's `include` and `exclude` options apply, and dependencies and Astro files are excluded. diff --git a/packages/astro/e2e/fixtures/react-compiler/astro.config.mjs b/packages/astro/e2e/fixtures/react-compiler/astro.config.mjs new file mode 100644 index 000000000000..bde98fe578c0 --- /dev/null +++ b/packages/astro/e2e/fixtures/react-compiler/astro.config.mjs @@ -0,0 +1,4 @@ +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ integrations: [react({ compiler: true })] }); diff --git a/packages/astro/e2e/fixtures/react-compiler/package.json b/packages/astro/e2e/fixtures/react-compiler/package.json new file mode 100644 index 000000000000..adbd7a10d998 --- /dev/null +++ b/packages/astro/e2e/fixtures/react-compiler/package.json @@ -0,0 +1,13 @@ +{ + "name": "@e2e/react-compiler", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/react": "workspace:*", + "astro": "workspace:*", + "oxc-transform-react": "^0.145.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-compiler-runtime": "^1.0.0" + } +} diff --git a/packages/astro/e2e/fixtures/react-compiler/src/components/Counter.jsx b/packages/astro/e2e/fixtures/react-compiler/src/components/Counter.jsx new file mode 100644 index 000000000000..105f3a04f5b0 --- /dev/null +++ b/packages/astro/e2e/fixtures/react-compiler/src/components/Counter.jsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; + +export default function Counter({ label }) { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/astro/e2e/fixtures/react-compiler/src/pages/index.astro b/packages/astro/e2e/fixtures/react-compiler/src/pages/index.astro new file mode 100644 index 000000000000..3df3d7318fc6 --- /dev/null +++ b/packages/astro/e2e/fixtures/react-compiler/src/pages/index.astro @@ -0,0 +1,9 @@ +--- +import Counter from '../components/Counter.jsx'; +--- + + + diff --git a/packages/astro/e2e/react-compiler.test.ts b/packages/astro/e2e/react-compiler.test.ts new file mode 100644 index 000000000000..303335b2e224 --- /dev/null +++ b/packages/astro/e2e/react-compiler.test.ts @@ -0,0 +1,47 @@ +import { expect } from '@playwright/test'; +import { testFactory, waitForHydrate, type DevServer, type PreviewServer } from './test-utils.ts'; + +const test = testFactory(import.meta.url, { root: './fixtures/react-compiler/' }); + +test.describe('React Compiler dev', () => { + let server: DevServer; + test.beforeAll(async ({ astro }) => { + server = await astro.startDevServer(); + }); + test.afterAll(async () => { + await server?.stop(); + }); + test('hydrates and preserves state through Fast Refresh', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); + await expect(page.locator('html')).toHaveAttribute('data-status', 'ready'); + const button = page.getByRole('button'); + await waitForHydrate(page, button); + await button.click(); + await expect(button).toHaveText('Count: 1'); + await astro.editFile('./src/components/Counter.jsx', (source) => + source.replace('{label}:', '{label} updated:'), + ); + await expect(button).toHaveText('Count updated: 1'); + await button.click(); + await expect(button).toHaveText('Count updated: 2'); + }); +}); + +test.describe('React Compiler build', () => { + let server: PreviewServer; + test.beforeAll(async ({ astro }) => { + await astro.build(); + server = await astro.preview(); + }); + test.afterAll(async () => { + await server?.stop(); + }); + test('hydrates the compiled production island', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); + await expect(page.locator('html')).toHaveAttribute('data-status', 'ready'); + const button = page.getByRole('button'); + await waitForHydrate(page, button); + await button.click(); + await expect(button).toHaveText('Count: 1'); + }); +}); diff --git a/packages/integrations/react/README.md b/packages/integrations/react/README.md index 9eba63ce58f3..2c46d2722c16 100644 --- a/packages/integrations/react/README.md +++ b/packages/integrations/react/README.md @@ -6,6 +6,29 @@ This **[Astro integration][astro-integration]** enables server-side rendering an Read the [`@astrojs/react` docs][docs] +## React Compiler (experimental) + +Install the optional Oxc compiler: + +```sh +pnpm add -D oxc-transform-react +``` + +Enable it in `astro.config.mjs`: + +```js +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [react({ compiler: true })], +}); +``` + +The compiler automatically memoizes client components and hooks. It does not compile server rendering. The target defaults to your installed React major version. For React 17 or 18, also install `react-compiler-runtime` as a runtime dependency. + +Pass an options object, such as `compiler: { compilationMode: 'annotation' }`, to configure Oxc's React Compiler. The integration's `include` and `exclude` filters apply; dependencies and Astro files are excluded. Compilation is disabled by default. The integration uses `@vitejs/plugin-react` v6 and Oxc for JSX and Fast Refresh. The `babel` integration option is removed. Projects requiring custom Babel plugins must configure `@rolldown/plugin-babel` separately under Astro’s `vite.plugins`; do not also enable `babel-plugin-react-compiler` on the same files. + ## Support - Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index c085c3da0f4d..a66d90408c03 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", - "@vitejs/plugin-react": "^5.2.0", + "@vitejs/plugin-react": "^6.1.0", "devalue": "^5.9.2", "ultrahtml": "^1.7.0", "vite": "^8.3.0" @@ -52,18 +52,25 @@ "astro-scripts": "workspace:*", "cheerio": "1.2.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "oxc-transform-react": "^0.145.0" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0", + "oxc-transform-react": "^0.145.0" }, "engines": { "node": ">=22.12.0" }, "publishConfig": { "provenance": true + }, + "peerDependenciesMeta": { + "oxc-transform-react": { + "optional": true + } } } diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index 64d1a1060ae2..83bae7359b09 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -17,7 +17,7 @@ import * as devalue from 'devalue'; export type ReactIntegrationOptions = Pick< ViteReactPluginOptions, - 'include' | 'exclude' | 'babel' + 'include' | 'exclude' | 'compiler' > & { experimentalReactChildren?: boolean; /** @@ -74,7 +74,7 @@ function getViteConfiguration( { include, exclude, - babel, + compiler, experimentalReactChildren, experimentalDisableStreaming, }: ReactIntegrationOptions = {}, @@ -85,16 +85,16 @@ function getViteConfiguration( // Without excluding .astro, the filter matches .astro virtual module scripts (e.g. // Foo.astro?astro&type=script&index=0&lang.ts) and forces lang to 'js', causing OXC // to fail parsing TypeScript syntax like `import type`. - const astroExclude = /\.astro$/; + const defaultExclude = [/\.astro$/, /\/node_modules\//]; const mergedExclude = exclude ? Array.isArray(exclude) - ? [...exclude, astroExclude] - : [exclude, astroExclude] - : astroExclude; + ? [...exclude, ...defaultExclude] + : [exclude, ...defaultExclude] + : defaultExclude; return { plugins: [ - react({ include, exclude: mergedExclude, babel }), + react({ include, exclude: mergedExclude, compiler }), optionsPlugin({ include, exclude, @@ -164,18 +164,23 @@ function configEnvironmentPlugin(reactConfig: ReactVersionConfig): vite.Plugin { }; } -export default function ({ - include, - exclude, - babel, - experimentalReactChildren, - experimentalDisableStreaming, -}: ReactIntegrationOptions = {}): AstroIntegration { +export default function (options: ReactIntegrationOptions = {}): AstroIntegration { + if ('babel' in options) { + throw new Error( + 'The @astrojs/react babel option has been removed. Configure @rolldown/plugin-babel in vite.plugins for custom Babel transforms.', + ); + } + const { include, exclude, compiler, experimentalReactChildren, experimentalDisableStreaming } = + options; const majorVersion = getReactMajorVersion(); if (!isSupportedReactVersion(majorVersion)) { throw new Error(`Unsupported React version: ${majorVersion}.`); } const versionConfig = versionsConfig[majorVersion]; + const compilerOptions = compiler && { + target: `${majorVersion}` as const, + ...(typeof compiler === 'object' ? compiler : {}), + }; return { name: '@astrojs/react', @@ -184,7 +189,13 @@ export default function ({ addRenderer(getRenderer(versionConfig)); updateConfig({ vite: getViteConfiguration( - { include, exclude, babel, experimentalReactChildren, experimentalDisableStreaming }, + { + include, + exclude, + compiler: compilerOptions, + experimentalReactChildren, + experimentalDisableStreaming, + }, versionConfig, ), }); diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/astro.config.mjs b/packages/integrations/react/test/fixtures/react-compiler-disabled/astro.config.mjs new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/astro.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/package.json b/packages/integrations/react/test/fixtures/react-compiler-disabled/package.json new file mode 100644 index 000000000000..6b87adf2f282 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/package.json @@ -0,0 +1,13 @@ +{ + "name": "@test/react-compiler-disabled", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/react": "workspace:*", + "astro": "workspace:*", + "oxc-transform-react": "^0.145.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-compiler-runtime": "^1.0.0" + } +} diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx new file mode 100644 index 000000000000..105f3a04f5b0 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; + +export default function Counter({ label }) { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro new file mode 100644 index 000000000000..ca9dc9a3afd4 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro @@ -0,0 +1,4 @@ +--- +import Counter from '../components/Counter.jsx'; +--- + diff --git a/packages/integrations/react/test/fixtures/react-compiler/astro.config.mjs b/packages/integrations/react/test/fixtures/react-compiler/astro.config.mjs new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler/astro.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/packages/integrations/react/test/fixtures/react-compiler/package.json b/packages/integrations/react/test/fixtures/react-compiler/package.json new file mode 100644 index 000000000000..f806e9ad5332 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler/package.json @@ -0,0 +1,13 @@ +{ + "name": "@test/react-compiler", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/react": "workspace:*", + "astro": "workspace:*", + "oxc-transform-react": "^0.145.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-compiler-runtime": "^1.0.0" + } +} diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx new file mode 100644 index 000000000000..105f3a04f5b0 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; + +export default function Counter({ label }) { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro new file mode 100644 index 000000000000..ca9dc9a3afd4 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro @@ -0,0 +1,4 @@ +--- +import Counter from '../components/Counter.jsx'; +--- + diff --git a/packages/integrations/react/test/react-compiler.test.ts b/packages/integrations/react/test/react-compiler.test.ts new file mode 100644 index 000000000000..abdc9a688304 --- /dev/null +++ b/packages/integrations/react/test/react-compiler.test.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { load } from 'cheerio'; +import react from '../dist/index.js'; +import { loadFixture, type DevServer, type Fixture } from './test-utils.ts'; + +for (const enabled of [true, false]) { + describe(`React compiler (${enabled ? 'enabled' : 'disabled'})`, () => { + let fixture: Fixture; + let server: DevServer; + before(async () => { + fixture = await loadFixture({ + root: new URL(`./fixtures/react-compiler${enabled ? '' : '-disabled'}/`, import.meta.url), + integrations: [react({ compiler: enabled })], + }); + }); + describe('dev', () => { + before(async () => { + server = await fixture.startDevServer(); + }); + after(async () => { + await server.stop(); + }); + it('compiles client components only when enabled', async () => { + const response = await fixture.fetch('/src/components/Counter.jsx'); + assert.equal(response.status, 200); + const code = await response.text(); + assert.equal(/react[-_]compiler[-_]runtime/.test(code), enabled); + }); + it('preserves server rendering', async () => { + const response = await fixture.fetch('/'); + assert.equal(response.status, 200); + assert.equal(load(await response.text())('button').text(), 'Count: 0'); + }); + }); + describe('build', () => { + before(async () => { + await fixture.build(); + }); + it('renders the island and emits a compiled client bundle only when enabled', async () => { + const $ = load(await fixture.readFile('/index.html')); + assert.equal($('button').text(), 'Count: 0'); + const component = $('astro-island').attr('component-url'); + assert.ok(component); + const code = await fixture.readFile(component); + assert.equal(code.includes('react.memo_cache_sentinel'), enabled); + }); + }); + }); +} + +describe('React integration migration', () => { + it('reports how to migrate the removed babel option', () => { + assert.throws( + () => Reflect.apply(react, undefined, [{ babel: {} }]), + /@rolldown\/plugin-babel in vite\.plugins/, + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d5f65ae9817..80ebfb092c01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1567,6 +1567,27 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/e2e/fixtures/react-compiler: + dependencies: + '@astrojs/react': + specifier: workspace:* + version: link:../../../../integrations/react + astro: + specifier: workspace:* + version: link:../../.. + oxc-transform-react: + specifier: ^0.145.0 + version: 0.145.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-compiler-runtime: + specifier: ^1.0.0 + version: 1.0.0(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + packages/astro/e2e/fixtures/react-component: dependencies: '@astrojs/mdx': @@ -5951,8 +5972,8 @@ importers: specifier: workspace:* version: link:../../internal-helpers '@vitejs/plugin-react': - specifier: ^5.2.0 - version: 5.2.0(supports-color@10.2.2)(vite@8.3.0) + specifier: ^6.1.0 + version: 6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0) devalue: specifier: ^5.9.2 version: 5.9.2 @@ -5978,6 +5999,9 @@ importers: cheerio: specifier: 1.2.0 version: 1.2.0 + oxc-transform-react: + specifier: ^0.145.0 + version: 0.145.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -6000,6 +6024,48 @@ importers: specifier: ^19.3.0 version: 19.3.0(react@19.3.0) + packages/integrations/react/test/fixtures/react-compiler: + dependencies: + '@astrojs/react': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + oxc-transform-react: + specifier: ^0.145.0 + version: 0.145.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-compiler-runtime: + specifier: ^1.0.0 + version: 1.0.0(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + + packages/integrations/react/test/fixtures/react-compiler-disabled: + dependencies: + '@astrojs/react': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + oxc-transform-react: + specifier: ^0.145.0 + version: 0.145.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-compiler-runtime: + specifier: ^1.0.0 + version: 1.0.0(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + packages/integrations/react/test/fixtures/react-component: dependencies: '@astrojs/react': @@ -7539,18 +7605,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.28.6': resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} engines: {node: '>=6.9.0'} @@ -9864,6 +9918,128 @@ packages: cpu: [x64] os: [win32] + '@oxc-transform-react/binding-android-arm-eabi@0.145.0': + resolution: {integrity: sha512-KuK3zAp10yFyeAxIXlZn2ycsFpaKLxUmj5BVRS1io4XfwnKTozb/goZakgKD5JEiHOVOtEKt1r7pTrmW/pFxxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-transform-react/binding-android-arm64@0.145.0': + resolution: {integrity: sha512-eDUKx6MAgRAF67JPScjLF6h8lL2qmlvFrXGSG8mYFIVJb++NMyDygiIo8TAKdWQNguoBElUZw/SK2EPRaKsryQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-transform-react/binding-darwin-arm64@0.145.0': + resolution: {integrity: sha512-msCgwbVLswJ08JaUOjfPddieVJwE4IT1Y23A1DDyKVyzZp2ormo9a4ffDEu9nmmovdEj8VqAyFgggsgzIkBVrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-transform-react/binding-darwin-x64@0.145.0': + resolution: {integrity: sha512-R0PtsoOCsARulmWhLG1fFInT+98TRl/H6w6ifi8XOLvpdsksD1rxsw4Ol84lHbdHCcMNjfJTckZzQIZBMiXkLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-transform-react/binding-freebsd-x64@0.145.0': + resolution: {integrity: sha512-/MJ7+IvxfutSGqn6hIXU92wGNk5FlkqbTHM+kEl7ytLLOSAyivYhF2gxdYMmXTwOG56k2XviMcE0D/TBjdyoRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-transform-react/binding-linux-arm-gnueabihf@0.145.0': + resolution: {integrity: sha512-ssHZ0W2wSjQwaBQdkg8u+A7dTdw2vkGSK0hRfCoDCkRkETu+19Q8Tae7NNxlYHcP1iUQYhEj8Qfgpq+S/MdrYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-transform-react/binding-linux-arm-musleabihf@0.145.0': + resolution: {integrity: sha512-adJy4lQVcoIW6CUzQYbDgbHZMDSLoizwCinG2Dex29jdACKwIgXQQrIAHAgfveaZleKIB/rf3usGoRcOGyfz7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-transform-react/binding-linux-arm64-gnu@0.145.0': + resolution: {integrity: sha512-7KRa0CC1FaSKx5sy+bDtqGvXF+RiVoMmSe8OmTKm9kRZkNVdmsmpnsQhgravehTohlWDveo5aoGMwcFtffGDmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-arm64-musl@0.145.0': + resolution: {integrity: sha512-fGhZkDnk2RoMp6THas9HFGorF0A1wwPPA/2Lu9IUO1ip5MJqS/JG7/conmiz0DBG0PWBtnKl2Y8sA/VBiD/oQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-linux-ppc64-gnu@0.145.0': + resolution: {integrity: sha512-VBaUTjRZYRypD2tL7cmbM2Fn6fvTnacLQ1GdsU1Y6A3fkqA/z+wc5Vy1QnnuFe9sIwb9xCvSS2tJVlucR39FYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-riscv64-gnu@0.145.0': + resolution: {integrity: sha512-s2cimMVAmWe9dtjyWdGsYtevi9C7gJoOXwrCZD0BX7hgyGmuBO3iWQjx4W48hdXlWS176NoxuFIHAN72N86upA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-riscv64-musl@0.145.0': + resolution: {integrity: sha512-hFPrsL2nKL4aa8vFmheR53PcrxQgym8bnaSD1pMX9IoyQq+/ksAuw1CDJSV9c5xhxxZdw/KTPVer7M/6OQvfoA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-linux-s390x-gnu@0.145.0': + resolution: {integrity: sha512-YLKhySqqu0JAtT0z8GuaFF7Hht3IvQLZShTH0DxeV28NwSthva+qJ7GN09ALT1bqRVvMjNGycgPHaGSWBUIt7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-x64-gnu@0.145.0': + resolution: {integrity: sha512-WqgJ+0M9mwQ1cRfSgk4vzYudD2imLSBhUZ2QFes06vUjigts5Dw4NajWKc2JibPHYsoiUA9Zj39zWwC5AxXEGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-transform-react/binding-linux-x64-musl@0.145.0': + resolution: {integrity: sha512-eIre/TjGt07IVKoWUrUOkPxN7UKeWxbiGvCywoGLDRe1/2K50MrA2vQahZ8AlW5CLmajOr9ePKrkRzLg1fa0cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-transform-react/binding-openharmony-arm64@0.145.0': + resolution: {integrity: sha512-zhWiyJ+9XD4cuhSZIT6aozuXDHcZHNdoVe88xVmx2XN00anjZ6VAGj31rhD8ndha/F8NllcQEr8Q61Dr2MHv5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-transform-react/binding-win32-arm64-msvc@0.145.0': + resolution: {integrity: sha512-Q0oiZfrfYyhmpqH2K0dylDTooIadecGsoiQks7G0AbuszCu2I7zYyJo4ArFc3d5Hj49Jjc5dDAE5WP/Sg2p8hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-transform-react/binding-win32-ia32-msvc@0.145.0': + resolution: {integrity: sha512-ERpriVJY5S8hXd3BJi4UtzsIVsEQnntmpqTgRqh0XCaflLRLTQnPWpkO8rgtI7xye5MRhfhA+qsxKg2olJs4qA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-transform-react/binding-win32-x64-msvc@0.145.0': + resolution: {integrity: sha512-sa781BGPLDw8+7ATrgUpLr0f4t0Q14O+zk3fOuQbUpjW5jERc9a3k1xIYKX1JkTG/sszoFhi8LzyCoMC49tkWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -10165,9 +10341,6 @@ packages: '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -10856,11 +11029,21 @@ packages: '@vercel/routing-utils@6.5.0': resolution: {integrity: sha512-2PXgATJyJYEaIuDEhljZ+sfOfJEsBl6OEzD+jHhy6NAb0k2mHdbTjcK3zTBNnYq0nebrXpq5ZGiwOxBFoUwB3A==} - '@vitejs/plugin-react@5.2.0': - resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true '@vitejs/plugin-vue-jsx@5.1.5': resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==} @@ -14266,6 +14449,10 @@ packages: oxc-resolver@11.17.1: resolution: {integrity: sha512-pyRXK9kH81zKlirHufkFhOFBZRks8iAMLwPH8gU7lvKFiuzUH9L8MxDEllazwOb8fjXMcWjY1PMDfMJ2/yh5cw==} + oxc-transform-react@0.145.0: + resolution: {integrity: sha512-y8cfjow11WKvDekonySPVvRUvNuIvqNT0LZ0Aey6j5dsNUD6k4hy6Mae/SLmng5egmb1GdJS11GduJ+IoEAXEw==} + engines: {node: ^20.19.0 || >=22.12.0} + p-event@6.0.1: resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} engines: {node: '>=16.17'} @@ -14821,6 +15008,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-compiler-runtime@1.0.0: + resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -14839,10 +15031,6 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -17235,16 +17423,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) @@ -19498,6 +19676,63 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.17.1': optional: true + '@oxc-transform-react/binding-android-arm-eabi@0.145.0': + optional: true + + '@oxc-transform-react/binding-android-arm64@0.145.0': + optional: true + + '@oxc-transform-react/binding-darwin-arm64@0.145.0': + optional: true + + '@oxc-transform-react/binding-darwin-x64@0.145.0': + optional: true + + '@oxc-transform-react/binding-freebsd-x64@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-arm-gnueabihf@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-arm-musleabihf@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-arm64-gnu@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-arm64-musl@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-ppc64-gnu@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-riscv64-gnu@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-riscv64-musl@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-s390x-gnu@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-x64-gnu@0.145.0': + optional: true + + '@oxc-transform-react/binding-linux-x64-musl@0.145.0': + optional: true + + '@oxc-transform-react/binding-openharmony-arm64@0.145.0': + optional: true + + '@oxc-transform-react/binding-win32-arm64-msvc@0.145.0': + optional: true + + '@oxc-transform-react/binding-win32-ia32-msvc@0.145.0': + optional: true + + '@oxc-transform-react/binding-win32-x64-msvc@0.145.0': + optional: true + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -19719,8 +19954,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rolldown/pluginutils@1.0.0-rc.3': {} - '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@4.2.1': @@ -20519,17 +20752,12 @@ snapshots: optionalDependencies: ajv: 6.15.0 - '@vitejs/plugin-react@5.2.0(supports-color@10.2.2)(vite@8.3.0)': + '@vitejs/plugin-react@6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0)': dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 + '@rolldown/pluginutils': 1.0.1 vite: 8.3.0(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.6.1)(sass@1.98.0)(tsx@4.23.13)(yaml@2.9.1) - transitivePeerDependencies: - - supports-color + optionalDependencies: + oxc-transform-react: 0.145.0 '@vitejs/plugin-vue-jsx@5.1.5(supports-color@10.2.2)(vite@8.3.0)(vue@3.5.42)': dependencies: @@ -24552,6 +24780,28 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + oxc-transform-react@0.145.0: + optionalDependencies: + '@oxc-transform-react/binding-android-arm-eabi': 0.145.0 + '@oxc-transform-react/binding-android-arm64': 0.145.0 + '@oxc-transform-react/binding-darwin-arm64': 0.145.0 + '@oxc-transform-react/binding-darwin-x64': 0.145.0 + '@oxc-transform-react/binding-freebsd-x64': 0.145.0 + '@oxc-transform-react/binding-linux-arm-gnueabihf': 0.145.0 + '@oxc-transform-react/binding-linux-arm-musleabihf': 0.145.0 + '@oxc-transform-react/binding-linux-arm64-gnu': 0.145.0 + '@oxc-transform-react/binding-linux-arm64-musl': 0.145.0 + '@oxc-transform-react/binding-linux-ppc64-gnu': 0.145.0 + '@oxc-transform-react/binding-linux-riscv64-gnu': 0.145.0 + '@oxc-transform-react/binding-linux-riscv64-musl': 0.145.0 + '@oxc-transform-react/binding-linux-s390x-gnu': 0.145.0 + '@oxc-transform-react/binding-linux-x64-gnu': 0.145.0 + '@oxc-transform-react/binding-linux-x64-musl': 0.145.0 + '@oxc-transform-react/binding-openharmony-arm64': 0.145.0 + '@oxc-transform-react/binding-win32-arm64-msvc': 0.145.0 + '@oxc-transform-react/binding-win32-ia32-msvc': 0.145.0 + '@oxc-transform-react/binding-win32-x64-msvc': 0.145.0 + p-event@6.0.1: dependencies: p-timeout: 6.1.4 @@ -25181,6 +25431,10 @@ snapshots: strip-json-comments: 2.0.1 optional: true + react-compiler-runtime@1.0.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -25199,8 +25453,6 @@ snapshots: react-is@17.0.2: {} - react-refresh@0.18.0: {} - react@18.3.1: dependencies: loose-envify: 1.4.0 From 0cfa29ed01ab60b0e0efa7af1f414af9a06a803e Mon Sep 17 00:00:00 2001 From: Jack Hanford Date: Wed, 9 Sep 2026 09:12:56 -0700 Subject: [PATCH 2/4] fix(react): preserve Babel compatibility for Oxc opt-in --- .changeset/friendly-tacos-compile.md | 10 ++- knip.js | 5 ++ packages/integrations/react/README.md | 2 +- packages/integrations/react/package.json | 2 +- packages/integrations/react/src/compiler.ts | 89 +++++++++++++++++++ packages/integrations/react/src/index.ts | 43 +++++---- .../react/test/compiler-plugin.test.ts | 43 +++++++++ .../src/components/Counter.jsx | 2 +- .../react-compiler/src/components/Counter.jsx | 2 +- .../react/test/react-compiler.test.ts | 57 ++++++++++-- pnpm-lock.yaml | 88 ++++++++++++------ 11 files changed, 283 insertions(+), 60 deletions(-) create mode 100644 packages/integrations/react/src/compiler.ts create mode 100644 packages/integrations/react/test/compiler-plugin.test.ts diff --git a/.changeset/friendly-tacos-compile.md b/.changeset/friendly-tacos-compile.md index b5648ba00ece..63b1a83697b8 100644 --- a/.changeset/friendly-tacos-compile.md +++ b/.changeset/friendly-tacos-compile.md @@ -1,10 +1,10 @@ --- -'@astrojs/react': major +'@astrojs/react': minor --- -Migrates the React integration to `@vitejs/plugin-react` v6, using Oxc for JSX and Fast Refresh. Removes the `babel` integration option; projects with custom Babel transforms must configure `@rolldown/plugin-babel` under `vite.plugins` instead. +Adds an opt-in `compiler` option for the Oxc-based React Compiler while preserving the existing `babel` option and React plugin behavior. -Adds an opt-in `compiler` option for the Oxc-based React Compiler. Install `oxc-transform-react` and enable it in your Astro config: +Install `oxc-transform-react` and enable it in your Astro config: ```js import react from '@astrojs/react'; @@ -14,4 +14,6 @@ export default { }; ``` -The compiler memoizes client components and hooks. Server rendering is not compiled. The target defaults to the installed React major version; React 17 and 18 projects also need `react-compiler-runtime` installed. Pass an options object to configure the compiler, for example `compiler: { compilationMode: 'annotation' }`. The integration's `include` and `exclude` options apply, and dependencies and Astro files are excluded. +The compiler memoizes client components and hooks. Server rendering is not compiled. The target defaults to the installed React major version; React 17 and 18 projects also need `react-compiler-runtime` installed. Pass an options object to configure the compiler, for example `compiler: { compilationMode: 'annotation' }`. The integration's `include` and `exclude` options apply to compilation; dependencies and Astro files are excluded from the Oxc transform. + +Existing Babel plugins continue to run. Do not enable the Babel React Compiler and Oxc React Compiler on the same client components; explicitly configuring both compilers produces an error. diff --git a/knip.js b/knip.js index 2594ed7111dd..443b5bd8a75f 100644 --- a/knip.js +++ b/knip.js @@ -111,6 +111,11 @@ export default { // never imports by name ignoreDependencies: ['@netlify/blobs'], }, + 'packages/integrations/react': { + entry: [srcEntry, dtsEntry, testEntry], + project, + ignoreDependencies: ['oxc-transform-react'], + }, 'packages/integrations/solid': { entry: [srcEntry, dtsEntry, testEntry], project, diff --git a/packages/integrations/react/README.md b/packages/integrations/react/README.md index 2c46d2722c16..5a135aa5f64b 100644 --- a/packages/integrations/react/README.md +++ b/packages/integrations/react/README.md @@ -27,7 +27,7 @@ export default defineConfig({ The compiler automatically memoizes client components and hooks. It does not compile server rendering. The target defaults to your installed React major version. For React 17 or 18, also install `react-compiler-runtime` as a runtime dependency. -Pass an options object, such as `compiler: { compilationMode: 'annotation' }`, to configure Oxc's React Compiler. The integration's `include` and `exclude` filters apply; dependencies and Astro files are excluded. Compilation is disabled by default. The integration uses `@vitejs/plugin-react` v6 and Oxc for JSX and Fast Refresh. The `babel` integration option is removed. Projects requiring custom Babel plugins must configure `@rolldown/plugin-babel` separately under Astro’s `vite.plugins`; do not also enable `babel-plugin-react-compiler` on the same files. +Pass an options object, such as `compiler: { compilationMode: 'annotation' }`, to configure Oxc's React Compiler. The integration's `include` and `exclude` filters apply; dependencies and Astro files are excluded. Compilation is disabled by default. Existing `babel` configuration and JSX/Fast Refresh behavior are preserved. The Oxc transform runs before the existing React plugin and preserves JSX for that pipeline. Do not also enable `babel-plugin-react-compiler` on the same client components; explicitly configuring both compilers produces an error. ## Support diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index a66d90408c03..5f798357ed7b 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", - "@vitejs/plugin-react": "^6.1.0", + "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.9.2", "ultrahtml": "^1.7.0", "vite": "^8.3.0" diff --git a/packages/integrations/react/src/compiler.ts b/packages/integrations/react/src/compiler.ts new file mode 100644 index 000000000000..401d9bf91d08 --- /dev/null +++ b/packages/integrations/react/src/compiler.ts @@ -0,0 +1,89 @@ +import type { BabelOptions, Options } from '@vitejs/plugin-react'; +import type { ReactCompilerOptions } from 'oxc-transform-react'; +import { createFilter, type Plugin } from 'vite'; +import { getReactMajorVersion } from './version.js'; + +export type CompilerOptions = ReactCompilerOptions; + +function checkBabelOptions(options: BabelOptions): BabelOptions { + for (const plugin of options.plugins ?? []) { + if (Array.isArray(plugin) && plugin[1] === false) continue; + const entry = Array.isArray(plugin) ? plugin[0] : plugin; + const name = typeof entry === 'string' ? entry : typeof entry === 'function' ? entry.name : ''; + if (name.includes('babel-plugin-react-compiler') || name === 'BabelPluginReactCompiler') { + throw new Error( + 'Enable only one React Compiler: remove babel-plugin-react-compiler or disable the @astrojs/react compiler option.', + ); + } + } + for (const override of options.overrides ?? []) checkBabelOptions(override); + for (const env of Object.values(options.env ?? {})) { + if (env) checkBabelOptions(env); + } + return options; +} + +export function withCompilerCheck(babel: Options['babel']): Options['babel'] { + if (typeof babel === 'function') { + return (id, options) => { + const result = babel(id, options); + return options.ssr ? result : checkBabelOptions(result); + }; + } + return babel && checkBabelOptions(babel); +} + +export function reactCompilerPlugin( + options: true | CompilerOptions, + { include, exclude }: Pick, +): Plugin { + const compilerOptions: CompilerOptions = { + target: String(getReactMajorVersion()) as '17' | '18' | '19', + ...(options === true ? {} : options), + }; + const filter = createFilter(include ?? /\.[jt]sx?$/, exclude); + let compiler: typeof import('oxc-transform-react'); + + return { + name: '@astrojs/react:compiler', + enforce: 'pre', + async config() { + try { + compiler = await import('oxc-transform-react'); + } catch (error) { + this.error( + `Could not load oxc-transform-react. Install it to use the @astrojs/react compiler option.\n${error instanceof Error ? error.message : String(error)}`, + ); + } + return { + optimizeDeps: { + include: [ + compilerOptions.target === '19' ? 'react/compiler-runtime' : 'react-compiler-runtime', + ], + }, + }; + }, + async transform(code, id, transformOptions) { + const filename = id.split('?')[0]; + if ( + transformOptions?.ssr || + this.environment.config.consumer === 'server' || + filename.includes('/node_modules/') || + !filter(filename) + ) + return; + const result = await compiler.transform(filename, code, { + jsx: 'preserve', + reactCompiler: compilerOptions, + sourcemap: true, + }); + if (result.fatal) { + this.error( + result.errors.map((error) => error.codeframe ?? error.message).join('\n') || + 'React Compiler transform failed.', + ); + } + return { code: result.code, map: result.map }; + }, + }; +} diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index 83bae7359b09..652704c7383b 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -1,3 +1,4 @@ +import { reactCompilerPlugin, withCompilerCheck, type CompilerOptions } from './compiler.js'; import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react'; import type { AstroIntegration, AstroRenderer } from 'astro'; import type * as vite from 'vite'; @@ -17,8 +18,9 @@ import * as devalue from 'devalue'; export type ReactIntegrationOptions = Pick< ViteReactPluginOptions, - 'include' | 'exclude' | 'compiler' + 'include' | 'exclude' | 'babel' > & { + compiler?: boolean | CompilerOptions; experimentalReactChildren?: boolean; /** * Disable streaming in React components @@ -74,6 +76,7 @@ function getViteConfiguration( { include, exclude, + babel, compiler, experimentalReactChildren, experimentalDisableStreaming, @@ -85,16 +88,21 @@ function getViteConfiguration( // Without excluding .astro, the filter matches .astro virtual module scripts (e.g. // Foo.astro?astro&type=script&index=0&lang.ts) and forces lang to 'js', causing OXC // to fail parsing TypeScript syntax like `import type`. - const defaultExclude = [/\.astro$/, /\/node_modules\//]; + const astroExclude = /\.astro$/; const mergedExclude = exclude ? Array.isArray(exclude) - ? [...exclude, ...defaultExclude] - : [exclude, ...defaultExclude] - : defaultExclude; + ? [...exclude, astroExclude] + : [exclude, astroExclude] + : astroExclude; return { plugins: [ - react({ include, exclude: mergedExclude, compiler }), + compiler && reactCompilerPlugin(compiler, { include, exclude: mergedExclude }), + react({ + include, + exclude: mergedExclude, + babel: compiler ? withCompilerCheck(babel) : babel, + }), optionsPlugin({ include, exclude, @@ -164,23 +172,19 @@ function configEnvironmentPlugin(reactConfig: ReactVersionConfig): vite.Plugin { }; } -export default function (options: ReactIntegrationOptions = {}): AstroIntegration { - if ('babel' in options) { - throw new Error( - 'The @astrojs/react babel option has been removed. Configure @rolldown/plugin-babel in vite.plugins for custom Babel transforms.', - ); - } - const { include, exclude, compiler, experimentalReactChildren, experimentalDisableStreaming } = - options; +export default function ({ + include, + exclude, + babel, + compiler, + experimentalReactChildren, + experimentalDisableStreaming, +}: ReactIntegrationOptions = {}): AstroIntegration { const majorVersion = getReactMajorVersion(); if (!isSupportedReactVersion(majorVersion)) { throw new Error(`Unsupported React version: ${majorVersion}.`); } const versionConfig = versionsConfig[majorVersion]; - const compilerOptions = compiler && { - target: `${majorVersion}` as const, - ...(typeof compiler === 'object' ? compiler : {}), - }; return { name: '@astrojs/react', @@ -192,7 +196,8 @@ export default function (options: ReactIntegrationOptions = {}): AstroIntegratio { include, exclude, - compiler: compilerOptions, + babel, + compiler, experimentalReactChildren, experimentalDisableStreaming, }, diff --git a/packages/integrations/react/test/compiler-plugin.test.ts b/packages/integrations/react/test/compiler-plugin.test.ts new file mode 100644 index 000000000000..85b2a1260626 --- /dev/null +++ b/packages/integrations/react/test/compiler-plugin.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { reactCompilerPlugin } from '../dist/compiler.js'; + +const code = 'export default function Greeting({ name }) { return

Hello {name}

; }'; + +describe('Oxc compiler transform', () => { + const plugin = reactCompilerPlugin(true, { exclude: [/\.astro$/, /excluded/] }); + const context = { + environment: { config: { consumer: 'client' } }, + error(message: string): never { + throw new Error(message); + }, + }; + before(async () => { + assert.ok(typeof plugin.config === 'function'); + await plugin.config.call(context as never, {}, {} as never); + }); + async function transform(id: string, ssr = false) { + assert.ok(typeof plugin.transform === 'function'); + return plugin.transform.call(context as never, code, id, { ssr, moduleType: 'jsx' }); + } + it('memoizes client JSX with the React 18 runtime and a source map', async () => { + const result = await transform('/src/Greeting.jsx?import'); + assert.ok(result && typeof result === 'object'); + assert.match(String(result.code), /react-compiler-runtime/); + assert.match(String(result.code), /\$\[/); + assert.ok(result.map); + }); + it('leaves server transforms untouched', async () => { + assert.equal(await transform('/src/Greeting.jsx', true), undefined); + }); + for (const id of [ + '/src/excluded.jsx', + '/node_modules/example/index.jsx', + '/src/Page.astro?astro&type=script&lang.ts', + '/src/style.css', + ]) { + it(`does not compile ${id}`, async () => { + assert.equal(await transform(id), undefined); + }); + } +}); diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx index 105f3a04f5b0..ed932f6e2f12 100644 --- a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx @@ -2,5 +2,5 @@ import { useState } from 'react'; export default function Counter({ label }) { const [count, setCount] = useState(0); - return ; + return ; } diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx index 105f3a04f5b0..ed932f6e2f12 100644 --- a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx +++ b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx @@ -2,5 +2,5 @@ import { useState } from 'react'; export default function Counter({ label }) { const [count, setCount] = useState(0); - return ; + return ; } diff --git a/packages/integrations/react/test/react-compiler.test.ts b/packages/integrations/react/test/react-compiler.test.ts index abdc9a688304..c5cbbae0b620 100644 --- a/packages/integrations/react/test/react-compiler.test.ts +++ b/packages/integrations/react/test/react-compiler.test.ts @@ -2,8 +2,21 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load } from 'cheerio'; import react from '../dist/index.js'; +import { withCompilerCheck } from '../dist/compiler.js'; import { loadFixture, type DevServer, type Fixture } from './test-utils.ts'; +const babelOptions = { + plugins: [ + () => ({ + visitor: { + StringLiteral(path: { node: { value: string } }) { + if (path.node.value === 'before-babel') path.node.value = 'after-babel'; + }, + }, + }), + ], +}; + for (const enabled of [true, false]) { describe(`React compiler (${enabled ? 'enabled' : 'disabled'})`, () => { let fixture: Fixture; @@ -11,7 +24,9 @@ for (const enabled of [true, false]) { before(async () => { fixture = await loadFixture({ root: new URL(`./fixtures/react-compiler${enabled ? '' : '-disabled'}/`, import.meta.url), - integrations: [react({ compiler: enabled })], + integrations: [ + react({ compiler: enabled, babel: enabled ? () => babelOptions : babelOptions }), + ], }); }); describe('dev', () => { @@ -26,11 +41,14 @@ for (const enabled of [true, false]) { assert.equal(response.status, 200); const code = await response.text(); assert.equal(/react[-_]compiler[-_]runtime/.test(code), enabled); + assert.match(code, /after-babel/); }); it('preserves server rendering', async () => { const response = await fixture.fetch('/'); assert.equal(response.status, 200); - assert.equal(load(await response.text())('button').text(), 'Count: 0'); + const $ = load(await response.text()); + assert.equal($('button').text(), 'Count: 0'); + assert.equal($('button').attr('data-babel'), 'after-babel'); }); }); describe('build', () => { @@ -40,6 +58,7 @@ for (const enabled of [true, false]) { it('renders the island and emits a compiled client bundle only when enabled', async () => { const $ = load(await fixture.readFile('/index.html')); assert.equal($('button').text(), 'Count: 0'); + assert.equal($('button').attr('data-babel'), 'after-babel'); const component = $('astro-island').attr('component-url'); assert.ok(component); const code = await fixture.readFile(component); @@ -49,11 +68,37 @@ for (const enabled of [true, false]) { }); } -describe('React integration migration', () => { - it('reports how to migrate the removed babel option', () => { +describe('React Compiler conflicts', () => { + it('rejects the Babel compiler in plugin lists and overrides', () => { + assert.throws( + () => withCompilerCheck({ plugins: ['babel-plugin-react-compiler'] }), + /Enable only one React Compiler/, + ); assert.throws( - () => Reflect.apply(react, undefined, [{ babel: {} }]), - /@rolldown\/plugin-babel in vite\.plugins/, + () => withCompilerCheck({ overrides: [{ plugins: [['babel-plugin-react-compiler', {}]] }] }), + /Enable only one React Compiler/, + ); + }); + it('checks Babel callbacks and preserves their arguments', () => { + const checked = withCompilerCheck((id, options) => { + assert.equal(id, '/src/Counter.jsx'); + assert.equal(options.ssr, false); + return { plugins: ['babel-plugin-react-compiler'] }; + }); + assert.ok(typeof checked === 'function'); + assert.throws( + () => checked('/src/Counter.jsx', { ssr: false }), + /Enable only one React Compiler/, + ); + }); + it('allows a Babel compiler callback confined to SSR', () => { + const checked = withCompilerCheck(() => ({ plugins: ['babel-plugin-react-compiler'] })); + assert.ok(typeof checked === 'function'); + assert.doesNotThrow(() => checked('/src/Counter.jsx', { ssr: true })); + }); + it('allows disabled Babel compiler entries', () => { + assert.doesNotThrow(() => + withCompilerCheck({ plugins: [['babel-plugin-react-compiler', false]] }), ); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80ebfb092c01..f515047489f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5972,8 +5972,8 @@ importers: specifier: workspace:* version: link:../../internal-helpers '@vitejs/plugin-react': - specifier: ^6.1.0 - version: 6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0) + specifier: ^5.2.0 + version: 5.2.0(supports-color@10.2.2)(vite@8.3.0) devalue: specifier: ^5.9.2 version: 5.9.2 @@ -7533,6 +7533,10 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.28.6': resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} engines: {node: '>=6.9.0'} @@ -7605,6 +7609,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.28.6': resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} engines: {node: '>=6.9.0'} @@ -10341,6 +10357,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -11029,21 +11048,11 @@ packages: '@vercel/routing-utils@6.5.0': resolution: {integrity: sha512-2PXgATJyJYEaIuDEhljZ+sfOfJEsBl6OEzD+jHhy6NAb0k2mHdbTjcK3zTBNnYq0nebrXpq5ZGiwOxBFoUwB3A==} - '@vitejs/plugin-react@6.1.1': - resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - oxc-transform-react: ^0.145.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - oxc-transform-react: - optional: true + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@vitejs/plugin-vue-jsx@5.1.5': resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==} @@ -15031,6 +15040,10 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -17351,6 +17364,8 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) @@ -17386,7 +17401,7 @@ snapshots: dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -17394,22 +17409,22 @@ snapshots: '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: @@ -17423,6 +17438,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0(supports-color@10.2.2) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0(supports-color@10.2.2) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) @@ -19954,6 +19979,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@4.2.1': @@ -20752,12 +20779,17 @@ snapshots: optionalDependencies: ajv: 6.15.0 - '@vitejs/plugin-react@6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0)': + '@vitejs/plugin-react@5.2.0(supports-color@10.2.2)(vite@8.3.0)': dependencies: - '@rolldown/pluginutils': 1.0.1 + '@babel/core': 7.29.0(supports-color@10.2.2) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 vite: 8.3.0(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.6.1)(sass@1.98.0)(tsx@4.23.13)(yaml@2.9.1) - optionalDependencies: - oxc-transform-react: 0.145.0 + transitivePeerDependencies: + - supports-color '@vitejs/plugin-vue-jsx@5.1.5(supports-color@10.2.2)(vite@8.3.0)(vue@3.5.42)': dependencies: @@ -21020,7 +21052,7 @@ snapshots: '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/template': 7.28.6 '@babel/traverse': 7.29.0(supports-color@10.2.2) @@ -21054,7 +21086,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.42 transitivePeerDependencies: @@ -21065,7 +21097,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.42 transitivePeerDependencies: @@ -25453,6 +25485,8 @@ snapshots: react-is@17.0.2: {} + react-refresh@0.18.0: {} + react@18.3.1: dependencies: loose-envify: 1.4.0 From f44be21f0e971c8a3b9ffa960c522e36fa228f3e Mon Sep 17 00:00:00 2001 From: Jack Hanford Date: Thu, 10 Sep 2026 10:58:26 -0700 Subject: [PATCH 3/4] feat(react)!: adopt Oxc and document Babel migration --- .changeset/friendly-tacos-compile.md | 52 +++++++- knip.js | 5 - packages/integrations/react/README.md | 46 ++++++- packages/integrations/react/package.json | 13 +- packages/integrations/react/src/compiler.ts | 89 ------------- packages/integrations/react/src/index.ts | 43 +++--- .../react/test/compiler-plugin.test.ts | 43 ------ .../src/components/Counter.jsx | 6 - .../src/components/Counter.tsx | 6 + .../src/pages/index.astro | 2 +- .../react-compiler/src/components/Counter.jsx | 6 - .../react-compiler/src/components/Counter.tsx | 6 + .../react-compiler/src/pages/index.astro | 2 +- .../react/test/react-compiler.test.ts | 54 +++----- pnpm-lock.yaml | 124 +++++++++--------- 15 files changed, 212 insertions(+), 285 deletions(-) delete mode 100644 packages/integrations/react/src/compiler.ts delete mode 100644 packages/integrations/react/test/compiler-plugin.test.ts delete mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx create mode 100644 packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.tsx delete mode 100644 packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx create mode 100644 packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.tsx diff --git a/.changeset/friendly-tacos-compile.md b/.changeset/friendly-tacos-compile.md index 63b1a83697b8..d2b68881c8fd 100644 --- a/.changeset/friendly-tacos-compile.md +++ b/.changeset/friendly-tacos-compile.md @@ -1,10 +1,10 @@ --- -'@astrojs/react': minor +'@astrojs/react': major --- -Adds an opt-in `compiler` option for the Oxc-based React Compiler while preserving the existing `babel` option and React plugin behavior. +Migrates the React integration to `@vitejs/plugin-react` v6, using Oxc for JSX and Fast Refresh. Removes the `babel` integration option; projects with custom Babel transforms must configure `@rolldown/plugin-babel` under `vite.plugins` instead. -Install `oxc-transform-react` and enable it in your Astro config: +Adds an opt-in `compiler` option for the Oxc-based React Compiler. Install `oxc-transform-react` and enable it in your Astro config: ```js import react from '@astrojs/react'; @@ -14,6 +14,48 @@ export default { }; ``` -The compiler memoizes client components and hooks. Server rendering is not compiled. The target defaults to the installed React major version; React 17 and 18 projects also need `react-compiler-runtime` installed. Pass an options object to configure the compiler, for example `compiler: { compilationMode: 'annotation' }`. The integration's `include` and `exclude` options apply to compilation; dependencies and Astro files are excluded from the Oxc transform. +The compiler memoizes client components and hooks. Server rendering is not compiled. The target defaults to the installed React major version; React 17 and 18 projects also need `react-compiler-runtime` installed. Pass an options object to configure the compiler, for example `compiler: { compilationMode: 'annotation' }`. The integration's `include` and `exclude` options apply, and dependencies and Astro files are excluded. -Existing Babel plugins continue to run. Do not enable the Babel React Compiler and Oxc React Compiler on the same client components; explicitly configuring both compilers produces an error. +## Migrating custom Babel transforms + +The integration uses `@vitejs/plugin-react` v6 and Oxc for JSX and Fast Refresh. The `babel` integration option has been removed in this major release. + +If you use custom Babel transforms, install `@rolldown/plugin-babel` and `@babel/core`, keeping your existing Babel plugins and presets installed: + +```sh +pnpm add -D @rolldown/plugin-babel @babel/core +``` + +Move your Babel plugins and presets from `react({ babel: ... })` to `babel()` in `vite.plugins`. For example, migrate a project using `babel-plugin-styled-components` from: + +```js +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [react({ babel: { plugins: ['babel-plugin-styled-components'] } })], +}); +``` + +To: + +```js +import react from '@astrojs/react'; +import babel from '@rolldown/plugin-babel'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [react()], + vite: { + plugins: [ + babel({ + plugins: ['babel-plugin-styled-components'], + }), + ], + }, +}); +``` + +This also works with `react({ compiler: true })`. Babel runs before Oxc transforms TypeScript and JSX. The Babel plugin automatically enables parsing for `.jsx`, `.ts`, and `.tsx` files. It does not load `babel.config.js` or `.babelrc` files; pass the options directly to `babel()`. The old `babel` callback is not supported; use the plugin's [`overrides` and preset hooks](https://github.com/rolldown/plugins/tree/main/packages/babel#options) for conditional transforms. + +Do not enable `babel-plugin-react-compiler` on the same components as the Oxc compiler. diff --git a/knip.js b/knip.js index 443b5bd8a75f..2594ed7111dd 100644 --- a/knip.js +++ b/knip.js @@ -111,11 +111,6 @@ export default { // never imports by name ignoreDependencies: ['@netlify/blobs'], }, - 'packages/integrations/react': { - entry: [srcEntry, dtsEntry, testEntry], - project, - ignoreDependencies: ['oxc-transform-react'], - }, 'packages/integrations/solid': { entry: [srcEntry, dtsEntry, testEntry], project, diff --git a/packages/integrations/react/README.md b/packages/integrations/react/README.md index 5a135aa5f64b..9d094c080dbd 100644 --- a/packages/integrations/react/README.md +++ b/packages/integrations/react/README.md @@ -27,7 +27,51 @@ export default defineConfig({ The compiler automatically memoizes client components and hooks. It does not compile server rendering. The target defaults to your installed React major version. For React 17 or 18, also install `react-compiler-runtime` as a runtime dependency. -Pass an options object, such as `compiler: { compilationMode: 'annotation' }`, to configure Oxc's React Compiler. The integration's `include` and `exclude` filters apply; dependencies and Astro files are excluded. Compilation is disabled by default. Existing `babel` configuration and JSX/Fast Refresh behavior are preserved. The Oxc transform runs before the existing React plugin and preserves JSX for that pipeline. Do not also enable `babel-plugin-react-compiler` on the same client components; explicitly configuring both compilers produces an error. +Pass an options object, such as `compiler: { compilationMode: 'annotation' }`, to configure Oxc's React Compiler. The integration's `include` and `exclude` filters apply; dependencies and Astro files are excluded. Compilation is disabled by default. + +## Migrating custom Babel transforms + +The integration uses `@vitejs/plugin-react` v6 and Oxc for JSX and Fast Refresh. The `babel` integration option has been removed in this major release. + +If you use custom Babel transforms, install `@rolldown/plugin-babel` and `@babel/core`, keeping your existing Babel plugins and presets installed: + +```sh +pnpm add -D @rolldown/plugin-babel @babel/core +``` + +Move your Babel plugins and presets from `react({ babel: ... })` to `babel()` in `vite.plugins`. For example, migrate a project using `babel-plugin-styled-components` from: + +```js +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [react({ babel: { plugins: ['babel-plugin-styled-components'] } })], +}); +``` + +To: + +```js +import react from '@astrojs/react'; +import babel from '@rolldown/plugin-babel'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [react()], + vite: { + plugins: [ + babel({ + plugins: ['babel-plugin-styled-components'], + }), + ], + }, +}); +``` + +This also works with `react({ compiler: true })`. Babel runs before Oxc transforms TypeScript and JSX. The Babel plugin automatically enables parsing for `.jsx`, `.ts`, and `.tsx` files. It does not load `babel.config.js` or `.babelrc` files; pass the options directly to `babel()`. The old `babel` callback is not supported; use the plugin's [`overrides` and preset hooks](https://github.com/rolldown/plugins/tree/main/packages/babel#options) for conditional transforms. + +Do not enable `babel-plugin-react-compiler` on the same components as the Oxc compiler. ## Support diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 5f798357ed7b..87f2037d5fb4 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -40,27 +40,30 @@ }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", - "@vitejs/plugin-react": "^5.2.0", + "@vitejs/plugin-react": "^6.1.0", "devalue": "^5.9.2", "ultrahtml": "^1.7.0", "vite": "^8.3.0" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@rolldown/plugin-babel": "^0.2.4", + "@types/babel__core": "^7.20.5", "@types/react": "^18.3.31", "@types/react-dom": "^18.3.7", "astro": "workspace:*", "astro-scripts": "workspace:*", "cheerio": "1.2.0", + "oxc-transform-react": "^0.145.0", "react": "^18.3.1", - "react-dom": "^18.3.1", - "oxc-transform-react": "^0.145.0" + "react-dom": "^18.3.1" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "oxc-transform-react": "^0.145.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0", - "oxc-transform-react": "^0.145.0" + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" }, "engines": { "node": ">=22.12.0" diff --git a/packages/integrations/react/src/compiler.ts b/packages/integrations/react/src/compiler.ts deleted file mode 100644 index 401d9bf91d08..000000000000 --- a/packages/integrations/react/src/compiler.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { BabelOptions, Options } from '@vitejs/plugin-react'; -import type { ReactCompilerOptions } from 'oxc-transform-react'; -import { createFilter, type Plugin } from 'vite'; -import { getReactMajorVersion } from './version.js'; - -export type CompilerOptions = ReactCompilerOptions; - -function checkBabelOptions(options: BabelOptions): BabelOptions { - for (const plugin of options.plugins ?? []) { - if (Array.isArray(plugin) && plugin[1] === false) continue; - const entry = Array.isArray(plugin) ? plugin[0] : plugin; - const name = typeof entry === 'string' ? entry : typeof entry === 'function' ? entry.name : ''; - if (name.includes('babel-plugin-react-compiler') || name === 'BabelPluginReactCompiler') { - throw new Error( - 'Enable only one React Compiler: remove babel-plugin-react-compiler or disable the @astrojs/react compiler option.', - ); - } - } - for (const override of options.overrides ?? []) checkBabelOptions(override); - for (const env of Object.values(options.env ?? {})) { - if (env) checkBabelOptions(env); - } - return options; -} - -export function withCompilerCheck(babel: Options['babel']): Options['babel'] { - if (typeof babel === 'function') { - return (id, options) => { - const result = babel(id, options); - return options.ssr ? result : checkBabelOptions(result); - }; - } - return babel && checkBabelOptions(babel); -} - -export function reactCompilerPlugin( - options: true | CompilerOptions, - { include, exclude }: Pick, -): Plugin { - const compilerOptions: CompilerOptions = { - target: String(getReactMajorVersion()) as '17' | '18' | '19', - ...(options === true ? {} : options), - }; - const filter = createFilter(include ?? /\.[jt]sx?$/, exclude); - let compiler: typeof import('oxc-transform-react'); - - return { - name: '@astrojs/react:compiler', - enforce: 'pre', - async config() { - try { - compiler = await import('oxc-transform-react'); - } catch (error) { - this.error( - `Could not load oxc-transform-react. Install it to use the @astrojs/react compiler option.\n${error instanceof Error ? error.message : String(error)}`, - ); - } - return { - optimizeDeps: { - include: [ - compilerOptions.target === '19' ? 'react/compiler-runtime' : 'react-compiler-runtime', - ], - }, - }; - }, - async transform(code, id, transformOptions) { - const filename = id.split('?')[0]; - if ( - transformOptions?.ssr || - this.environment.config.consumer === 'server' || - filename.includes('/node_modules/') || - !filter(filename) - ) - return; - const result = await compiler.transform(filename, code, { - jsx: 'preserve', - reactCompiler: compilerOptions, - sourcemap: true, - }); - if (result.fatal) { - this.error( - result.errors.map((error) => error.codeframe ?? error.message).join('\n') || - 'React Compiler transform failed.', - ); - } - return { code: result.code, map: result.map }; - }, - }; -} diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts index 652704c7383b..83bae7359b09 100644 --- a/packages/integrations/react/src/index.ts +++ b/packages/integrations/react/src/index.ts @@ -1,4 +1,3 @@ -import { reactCompilerPlugin, withCompilerCheck, type CompilerOptions } from './compiler.js'; import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react'; import type { AstroIntegration, AstroRenderer } from 'astro'; import type * as vite from 'vite'; @@ -18,9 +17,8 @@ import * as devalue from 'devalue'; export type ReactIntegrationOptions = Pick< ViteReactPluginOptions, - 'include' | 'exclude' | 'babel' + 'include' | 'exclude' | 'compiler' > & { - compiler?: boolean | CompilerOptions; experimentalReactChildren?: boolean; /** * Disable streaming in React components @@ -76,7 +74,6 @@ function getViteConfiguration( { include, exclude, - babel, compiler, experimentalReactChildren, experimentalDisableStreaming, @@ -88,21 +85,16 @@ function getViteConfiguration( // Without excluding .astro, the filter matches .astro virtual module scripts (e.g. // Foo.astro?astro&type=script&index=0&lang.ts) and forces lang to 'js', causing OXC // to fail parsing TypeScript syntax like `import type`. - const astroExclude = /\.astro$/; + const defaultExclude = [/\.astro$/, /\/node_modules\//]; const mergedExclude = exclude ? Array.isArray(exclude) - ? [...exclude, astroExclude] - : [exclude, astroExclude] - : astroExclude; + ? [...exclude, ...defaultExclude] + : [exclude, ...defaultExclude] + : defaultExclude; return { plugins: [ - compiler && reactCompilerPlugin(compiler, { include, exclude: mergedExclude }), - react({ - include, - exclude: mergedExclude, - babel: compiler ? withCompilerCheck(babel) : babel, - }), + react({ include, exclude: mergedExclude, compiler }), optionsPlugin({ include, exclude, @@ -172,19 +164,23 @@ function configEnvironmentPlugin(reactConfig: ReactVersionConfig): vite.Plugin { }; } -export default function ({ - include, - exclude, - babel, - compiler, - experimentalReactChildren, - experimentalDisableStreaming, -}: ReactIntegrationOptions = {}): AstroIntegration { +export default function (options: ReactIntegrationOptions = {}): AstroIntegration { + if ('babel' in options) { + throw new Error( + 'The @astrojs/react babel option has been removed. Configure @rolldown/plugin-babel in vite.plugins for custom Babel transforms.', + ); + } + const { include, exclude, compiler, experimentalReactChildren, experimentalDisableStreaming } = + options; const majorVersion = getReactMajorVersion(); if (!isSupportedReactVersion(majorVersion)) { throw new Error(`Unsupported React version: ${majorVersion}.`); } const versionConfig = versionsConfig[majorVersion]; + const compilerOptions = compiler && { + target: `${majorVersion}` as const, + ...(typeof compiler === 'object' ? compiler : {}), + }; return { name: '@astrojs/react', @@ -196,8 +192,7 @@ export default function ({ { include, exclude, - babel, - compiler, + compiler: compilerOptions, experimentalReactChildren, experimentalDisableStreaming, }, diff --git a/packages/integrations/react/test/compiler-plugin.test.ts b/packages/integrations/react/test/compiler-plugin.test.ts deleted file mode 100644 index 85b2a1260626..000000000000 --- a/packages/integrations/react/test/compiler-plugin.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import { reactCompilerPlugin } from '../dist/compiler.js'; - -const code = 'export default function Greeting({ name }) { return

Hello {name}

; }'; - -describe('Oxc compiler transform', () => { - const plugin = reactCompilerPlugin(true, { exclude: [/\.astro$/, /excluded/] }); - const context = { - environment: { config: { consumer: 'client' } }, - error(message: string): never { - throw new Error(message); - }, - }; - before(async () => { - assert.ok(typeof plugin.config === 'function'); - await plugin.config.call(context as never, {}, {} as never); - }); - async function transform(id: string, ssr = false) { - assert.ok(typeof plugin.transform === 'function'); - return plugin.transform.call(context as never, code, id, { ssr, moduleType: 'jsx' }); - } - it('memoizes client JSX with the React 18 runtime and a source map', async () => { - const result = await transform('/src/Greeting.jsx?import'); - assert.ok(result && typeof result === 'object'); - assert.match(String(result.code), /react-compiler-runtime/); - assert.match(String(result.code), /\$\[/); - assert.ok(result.map); - }); - it('leaves server transforms untouched', async () => { - assert.equal(await transform('/src/Greeting.jsx', true), undefined); - }); - for (const id of [ - '/src/excluded.jsx', - '/node_modules/example/index.jsx', - '/src/Page.astro?astro&type=script&lang.ts', - '/src/style.css', - ]) { - it(`does not compile ${id}`, async () => { - assert.equal(await transform(id), undefined); - }); - } -}); diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx deleted file mode 100644 index ed932f6e2f12..000000000000 --- a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useState } from 'react'; - -export default function Counter({ label }) { - const [count, setCount] = useState(0); - return ; -} diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.tsx b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.tsx new file mode 100644 index 000000000000..ebb599f26d27 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/components/Counter.tsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; + +export default function Counter({ label }: { label: string }) { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro index ca9dc9a3afd4..4c8a3ac41e03 100644 --- a/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro +++ b/packages/integrations/react/test/fixtures/react-compiler-disabled/src/pages/index.astro @@ -1,4 +1,4 @@ --- -import Counter from '../components/Counter.jsx'; +import Counter from '../components/Counter.tsx'; --- diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx deleted file mode 100644 index ed932f6e2f12..000000000000 --- a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useState } from 'react'; - -export default function Counter({ label }) { - const [count, setCount] = useState(0); - return ; -} diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.tsx b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.tsx new file mode 100644 index 000000000000..ebb599f26d27 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-compiler/src/components/Counter.tsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; + +export default function Counter({ label }: { label: string }) { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro index ca9dc9a3afd4..4c8a3ac41e03 100644 --- a/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro +++ b/packages/integrations/react/test/fixtures/react-compiler/src/pages/index.astro @@ -1,4 +1,4 @@ --- -import Counter from '../components/Counter.jsx'; +import Counter from '../components/Counter.tsx'; --- diff --git a/packages/integrations/react/test/react-compiler.test.ts b/packages/integrations/react/test/react-compiler.test.ts index c5cbbae0b620..bdf5381f415d 100644 --- a/packages/integrations/react/test/react-compiler.test.ts +++ b/packages/integrations/react/test/react-compiler.test.ts @@ -1,16 +1,20 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PluginObj } from '@babel/core'; +import babel from '@rolldown/plugin-babel'; import { load } from 'cheerio'; import react from '../dist/index.js'; -import { withCompilerCheck } from '../dist/compiler.js'; import { loadFixture, type DevServer, type Fixture } from './test-utils.ts'; const babelOptions = { plugins: [ - () => ({ + (): PluginObj => ({ visitor: { - StringLiteral(path: { node: { value: string } }) { - if (path.node.value === 'before-babel') path.node.value = 'after-babel'; + TSAsExpression(path) { + const expression = path.node.expression; + if (expression.type === 'StringLiteral' && expression.value === 'before-babel') { + expression.value = 'after-babel'; + } }, }, }), @@ -24,9 +28,8 @@ for (const enabled of [true, false]) { before(async () => { fixture = await loadFixture({ root: new URL(`./fixtures/react-compiler${enabled ? '' : '-disabled'}/`, import.meta.url), - integrations: [ - react({ compiler: enabled, babel: enabled ? () => babelOptions : babelOptions }), - ], + integrations: [react({ compiler: enabled })], + vite: { plugins: [babel(babelOptions)] }, }); }); describe('dev', () => { @@ -37,7 +40,7 @@ for (const enabled of [true, false]) { await server.stop(); }); it('compiles client components only when enabled', async () => { - const response = await fixture.fetch('/src/components/Counter.jsx'); + const response = await fixture.fetch('/src/components/Counter.tsx'); assert.equal(response.status, 200); const code = await response.text(); assert.equal(/react[-_]compiler[-_]runtime/.test(code), enabled); @@ -63,42 +66,17 @@ for (const enabled of [true, false]) { assert.ok(component); const code = await fixture.readFile(component); assert.equal(code.includes('react.memo_cache_sentinel'), enabled); + assert.match(code, /after-babel/); }); }); }); } -describe('React Compiler conflicts', () => { - it('rejects the Babel compiler in plugin lists and overrides', () => { - assert.throws( - () => withCompilerCheck({ plugins: ['babel-plugin-react-compiler'] }), - /Enable only one React Compiler/, - ); - assert.throws( - () => withCompilerCheck({ overrides: [{ plugins: [['babel-plugin-react-compiler', {}]] }] }), - /Enable only one React Compiler/, - ); - }); - it('checks Babel callbacks and preserves their arguments', () => { - const checked = withCompilerCheck((id, options) => { - assert.equal(id, '/src/Counter.jsx'); - assert.equal(options.ssr, false); - return { plugins: ['babel-plugin-react-compiler'] }; - }); - assert.ok(typeof checked === 'function'); +describe('React integration migration', () => { + it('reports how to migrate the removed babel option', () => { assert.throws( - () => checked('/src/Counter.jsx', { ssr: false }), - /Enable only one React Compiler/, - ); - }); - it('allows a Babel compiler callback confined to SSR', () => { - const checked = withCompilerCheck(() => ({ plugins: ['babel-plugin-react-compiler'] })); - assert.ok(typeof checked === 'function'); - assert.doesNotThrow(() => checked('/src/Counter.jsx', { ssr: true })); - }); - it('allows disabled Babel compiler entries', () => { - assert.doesNotThrow(() => - withCompilerCheck({ plugins: [['babel-plugin-react-compiler', false]] }), + () => Reflect.apply(react, undefined, [{ babel: {} }]), + /@rolldown\/plugin-babel in vite\.plugins/, ); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f515047489f5..9dfdb8c99c0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5972,8 +5972,8 @@ importers: specifier: workspace:* version: link:../../internal-helpers '@vitejs/plugin-react': - specifier: ^5.2.0 - version: 5.2.0(supports-color@10.2.2)(vite@8.3.0) + specifier: ^6.1.0 + version: 6.1.1(@rolldown/plugin-babel@0.2.4)(oxc-transform-react@0.145.0)(vite@8.3.0) devalue: specifier: ^5.9.2 version: 5.9.2 @@ -5984,6 +5984,15 @@ importers: specifier: ^8.3.0 version: 8.3.0(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.6.1)(sass@1.98.0)(tsx@4.23.13)(yaml@2.9.1) devDependencies: + '@babel/core': + specifier: ^7.29.0 + version: 7.29.0(supports-color@10.2.2) + '@rolldown/plugin-babel': + specifier: ^0.2.4 + version: 0.2.4(@babel/core@7.29.0)(@babel/runtime@7.28.6)(rolldown@1.2.8)(vite@8.3.0) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 '@types/react': specifier: ^18.3.31 version: 18.3.31 @@ -7533,10 +7542,6 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - '@babel/helper-replace-supers@7.28.6': resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} engines: {node: '>=6.9.0'} @@ -7609,18 +7614,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.29.7': - resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.29.7': - resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.28.6': resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} engines: {node: '>=6.9.0'} @@ -10354,12 +10347,26 @@ packages: cpu: [x64] os: [win32] + '@rolldown/plugin-babel@0.2.4': + resolution: {integrity: sha512-ygmm2j/mN+aCvstHo0x3IOu4uc6LqXT9SJ3q4kpfG/+Yhfz0B+E47FITcC0FljZADwUFARP6E2kiE+Lc26D8wg==} + engines: {node: '>=22.12.0 || ^24.0.0'} + peerDependencies: + '@babel/core': ^7.29.0 || ^8.0.0-rc.1 + '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1 + '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + '@babel/plugin-transform-runtime': + optional: true + '@babel/runtime': + optional: true + vite: + optional: true + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -11048,11 +11055,21 @@ packages: '@vercel/routing-utils@6.5.0': resolution: {integrity: sha512-2PXgATJyJYEaIuDEhljZ+sfOfJEsBl6OEzD+jHhy6NAb0k2mHdbTjcK3zTBNnYq0nebrXpq5ZGiwOxBFoUwB3A==} - '@vitejs/plugin-react@5.2.0': - resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true '@vitejs/plugin-vue-jsx@5.1.5': resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==} @@ -15040,10 +15057,6 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -17364,8 +17377,6 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) @@ -17401,7 +17412,7 @@ snapshots: dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -17409,22 +17420,22 @@ snapshots: '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: @@ -17438,16 +17449,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) @@ -19977,9 +19978,16 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.2.8': optional: true - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/plugin-babel@0.2.4(@babel/core@7.29.0)(@babel/runtime@7.28.6)(rolldown@1.2.8)(vite@8.3.0)': + dependencies: + '@babel/core': 7.29.0(supports-color@10.2.2) + picomatch: 4.0.7 + rolldown: 1.2.8 + optionalDependencies: + '@babel/runtime': 7.28.6 + vite: 8.3.0(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.6.1)(sass@1.98.0)(tsx@4.23.13)(yaml@2.9.1) - '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} '@rolldown/pluginutils@1.0.1': {} @@ -20779,17 +20787,13 @@ snapshots: optionalDependencies: ajv: 6.15.0 - '@vitejs/plugin-react@5.2.0(supports-color@10.2.2)(vite@8.3.0)': + '@vitejs/plugin-react@6.1.1(@rolldown/plugin-babel@0.2.4)(oxc-transform-react@0.145.0)(vite@8.3.0)': dependencies: - '@babel/core': 7.29.0(supports-color@10.2.2) - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 + '@rolldown/pluginutils': 1.0.1 vite: 8.3.0(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.6.1)(sass@1.98.0)(tsx@4.23.13)(yaml@2.9.1) - transitivePeerDependencies: - - supports-color + optionalDependencies: + '@rolldown/plugin-babel': 0.2.4(@babel/core@7.29.0)(@babel/runtime@7.28.6)(rolldown@1.2.8)(vite@8.3.0) + oxc-transform-react: 0.145.0 '@vitejs/plugin-vue-jsx@5.1.5(supports-color@10.2.2)(vite@8.3.0)(vue@3.5.42)': dependencies: @@ -21052,7 +21056,7 @@ snapshots: '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)(supports-color@10.2.2)': dependencies: '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/template': 7.28.6 '@babel/traverse': 7.29.0(supports-color@10.2.2) @@ -21086,7 +21090,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.42 transitivePeerDependencies: @@ -21097,7 +21101,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/helper-module-imports': 7.28.6(supports-color@10.2.2) - '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-plugin-utils': 7.28.6 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.42 transitivePeerDependencies: @@ -25485,8 +25489,6 @@ snapshots: react-is@17.0.2: {} - react-refresh@0.18.0: {} - react@18.3.1: dependencies: loose-envify: 1.4.0 From 0b7f8c4a580fbcd160e727fec545d690b5c6c992 Mon Sep 17 00:00:00 2001 From: Jack Hanford Date: Thu, 17 Sep 2026 10:09:38 -0700 Subject: [PATCH 4/4] Update packages/integrations/react/README.md Co-authored-by: Matthew Phillips --- packages/integrations/react/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/react/README.md b/packages/integrations/react/README.md index 9d094c080dbd..c6055f1495a9 100644 --- a/packages/integrations/react/README.md +++ b/packages/integrations/react/README.md @@ -6,7 +6,7 @@ This **[Astro integration][astro-integration]** enables server-side rendering an Read the [`@astrojs/react` docs][docs] -## React Compiler (experimental) +## React Compiler Install the optional Oxc compiler: