diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md index b597e2d1..bcb6fccc 100644 --- a/packages/plugin-react/CHANGELOG.md +++ b/packages/plugin-react/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Fix support for React Compiler with React 18 + +The previous version made this assumption that the compiler was only usable with React 19, but it's possible to use it with React 18 and a custom `runtimeModule`: https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43 + +When using a custom `runtimeModule`, the plugin will not try to pre-optimize `react/compiler-runtime` dependency. + +Reminder: Vite expect code outside of `node_modules` to be ESM, so you will need to update the gist with `import React from 'react'`. + ## 4.3.0 (2024-05-22) ### Fix support for React compiler diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 1598901c..9d2356e3 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -274,7 +274,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { const dependencies = ['react', jsxImportDevRuntime, jsxImportRuntime] const staticBabelPlugins = typeof opts.babel === 'object' ? opts.babel?.plugins ?? [] : [] - if (hasCompiler(staticBabelPlugins)) { + if (hasCompilerWithDefaultRuntime(staticBabelPlugins)) { dependencies.push('react/compiler-runtime') } @@ -376,3 +376,14 @@ function hasCompiler(plugins: ReactBabelOptions['plugins']) { (Array.isArray(p) && p[0] === 'babel-plugin-react-compiler'), ) } + +// https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43 +function hasCompilerWithDefaultRuntime(plugins: ReactBabelOptions['plugins']) { + return plugins.some( + (p) => + p === 'babel-plugin-react-compiler' || + (Array.isArray(p) && + p[0] === 'babel-plugin-react-compiler' && + p[1]?.runtimeModule === undefined), + ) +} diff --git a/playground/compiler-react-18/__tests__/compiler-react-18.spec.ts b/playground/compiler-react-18/__tests__/compiler-react-18.spec.ts new file mode 100644 index 00000000..545505ff --- /dev/null +++ b/playground/compiler-react-18/__tests__/compiler-react-18.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { editFile, isServe, page, untilUpdated } from '~utils' + +test('should render', async () => { + expect(await page.textContent('button')).toMatch('count is 0') + expect(await page.click('button')) + expect(await page.textContent('button')).toMatch('count is 1') +}) + +test.runIf(isServe)('should hmr', async () => { + editFile('src/App.tsx', (code) => + code.replace('count is {count}', 'count is {count}!'), + ) + await untilUpdated(() => page.textContent('button'), 'count is 1!') +}) diff --git a/playground/compiler-react-18/index.html b/playground/compiler-react-18/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/playground/compiler-react-18/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/playground/compiler-react-18/lib/react-compiler-runtime/index.js b/playground/compiler-react-18/lib/react-compiler-runtime/index.js new file mode 100644 index 00000000..3d794bf7 --- /dev/null +++ b/playground/compiler-react-18/lib/react-compiler-runtime/index.js @@ -0,0 +1,20 @@ +import React from 'react' +const $empty = Symbol.for('react.memo_cache_sentinel') +/** + * DANGER: this hook is NEVER meant to be called directly! + * + * Note that this is a temporary userspace implementation of this function + * from React 19. It is not as efficient and may invalidate more frequently + * than the official API. Please upgrade to React 19 as soon as you can. + **/ +export function c(size) { + return React.useState(() => { + const $ = new Array(size) + for (let ii = 0; ii < size; ii++) { + $[ii] = $empty + } + // @ts-ignore + $[$empty] = true + return $ + })[0] +} diff --git a/playground/compiler-react-18/lib/react-compiler-runtime/package.json b/playground/compiler-react-18/lib/react-compiler-runtime/package.json new file mode 100644 index 00000000..372531f5 --- /dev/null +++ b/playground/compiler-react-18/lib/react-compiler-runtime/package.json @@ -0,0 +1,8 @@ +{ + "name": "react-compiler-runtime", + "private": true, + "main": "index.js", + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/playground/compiler-react-18/package.json b/playground/compiler-react-18/package.json new file mode 100644 index 00000000..4080304a --- /dev/null +++ b/playground/compiler-react-18/package.json @@ -0,0 +1,24 @@ +{ + "name": "@vitejs/test-compiler-react-18", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-compiler-runtime": "file:./lib/react-compiler-runtime", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "workspace:*", + "babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/playground/compiler-react-18/public/vite.svg b/playground/compiler-react-18/public/vite.svg new file mode 100644 index 00000000..ee9fadaf --- /dev/null +++ b/playground/compiler-react-18/public/vite.svg @@ -0,0 +1 @@ + diff --git a/playground/compiler-react-18/src/App.css b/playground/compiler-react-18/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/playground/compiler-react-18/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/playground/compiler-react-18/src/App.tsx b/playground/compiler-react-18/src/App.tsx new file mode 100644 index 00000000..5ac9bad2 --- /dev/null +++ b/playground/compiler-react-18/src/App.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react' +import './App.css' + +export function App() { + const [count, setCount] = useState(0) + + return ( + <> +

Vite + React 18 + compiler

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} diff --git a/playground/compiler-react-18/src/index.css b/playground/compiler-react-18/src/index.css new file mode 100644 index 00000000..2c3fac68 --- /dev/null +++ b/playground/compiler-react-18/src/index.css @@ -0,0 +1,69 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/playground/compiler-react-18/src/main.tsx b/playground/compiler-react-18/src/main.tsx new file mode 100644 index 00000000..813e3d76 --- /dev/null +++ b/playground/compiler-react-18/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { App } from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/playground/compiler-react-18/tsconfig.json b/playground/compiler-react-18/tsconfig.json new file mode 100644 index 00000000..fac7d251 --- /dev/null +++ b/playground/compiler-react-18/tsconfig.json @@ -0,0 +1,25 @@ +{ + "include": ["src"], + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/playground/compiler-react-18/vite.config.ts b/playground/compiler-react-18/vite.config.ts new file mode 100644 index 00000000..36403615 --- /dev/null +++ b/playground/compiler-react-18/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43 + +// https://vitejs.dev/config/ +export default defineConfig(({ command }) => { + return { + server: { port: 8901 /* Should be unique */ }, + plugins: [ + react({ + babel: { + plugins: [ + [ + 'babel-plugin-react-compiler', + { runtimeModule: 'react-compiler-runtime' }, + ], + ], + }, + }), + ], + } +}) diff --git a/playground/compiler/package.json b/playground/compiler/package.json index 6b8f0e70..8faac8da 100644 --- a/playground/compiler/package.json +++ b/playground/compiler/package.json @@ -1,5 +1,5 @@ { - "name": "@vitejs/compiler", + "name": "@vitejs/test-compiler", "private": true, "type": "module", "scripts": { diff --git a/playground/compiler/src/App.tsx b/playground/compiler/src/App.tsx index c2d2e8ac..3cdc1d62 100644 --- a/playground/compiler/src/App.tsx +++ b/playground/compiler/src/App.tsx @@ -6,7 +6,7 @@ export function App() { return ( <> -

Vite + React

+

Vite + React Compiler