├── pnpm-workspace.yaml ├── .gitignore ├── playground ├── class-components │ ├── src │ │ ├── utils.tsx │ │ ├── App.tsx │ │ └── index.tsx │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── __tests__ │ │ └── class-components.spec.ts │ └── public │ │ └── vite.svg ├── ts-lib │ ├── src │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── about.tsx │ │ │ ├── index.tsx │ │ │ └── _app.tsx │ │ └── index.tsx │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── __tests__ │ │ └── ts-lib.spec.ts │ └── public │ │ └── vite.svg ├── mdx │ ├── src │ │ ├── env.d.ts │ │ ├── hello.mdx │ │ ├── Counter.tsx │ │ └── index.tsx │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── mdx.spec.ts ├── hmr │ ├── src │ │ ├── TitleWithExport.tsx │ │ ├── index.tsx │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ └── react.svg │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── hmr.spec.ts ├── react-18 │ ├── src │ │ ├── TitleWithExport.tsx │ │ ├── index.tsx │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ └── react.svg │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── react-18.spec.ts ├── worker │ ├── src │ │ ├── worker-via-url.ts │ │ ├── worker-via-import.ts │ │ ├── App.tsx │ │ └── index.tsx │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── __tests__ │ │ └── worker.spec.ts │ └── public │ │ └── vite.svg ├── shadow-export │ ├── vite.config.ts │ ├── src │ │ ├── index.tsx │ │ └── App.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── __tests__ │ │ └── shadow-export.spec.ts │ └── public │ │ └── vite.svg ├── decorators │ ├── vite.config.ts │ ├── src │ │ ├── index.tsx │ │ └── App.tsx │ ├── index.html │ ├── package.json │ ├── __tests__ │ │ └── decorators.spec.ts │ ├── tsconfig.json │ └── public │ │ └── vite.svg ├── base-path │ ├── vite.config.ts │ ├── src │ │ ├── App.tsx │ │ └── index.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── __tests__ │ │ └── base-path.spec.ts │ └── public │ │ └── vite.svg ├── emotion │ ├── vite.config.ts │ ├── src │ │ ├── index.tsx │ │ ├── App.css │ │ ├── index.css │ │ ├── Button.tsx │ │ └── App.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── emotion.spec.ts ├── styled-components │ ├── vite.config.ts │ ├── src │ │ ├── index.tsx │ │ ├── App.css │ │ ├── index.css │ │ ├── Button.tsx │ │ └── App.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── styled-components.spec.ts ├── emotion-plugin │ ├── src │ │ ├── index.jsx │ │ ├── App.css │ │ ├── index.css │ │ ├── Button.jsx │ │ └── App.jsx │ ├── vite.config.js │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ └── __tests__ │ │ └── emotion-plugin.spec.ts └── utils.ts ├── scripts ├── publish.ts ├── release.ts └── bundle.ts ├── README.md ├── .github ├── workflows │ ├── ci.yml │ └── publish.yml └── renovate.json5 ├── playwright.config.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── CHANGELOG.md └── src ├── index.ts └── refresh-runtime.js /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'playground/**' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | playground-temp/ 5 | test-results/ 6 | .swc/ 7 | -------------------------------------------------------------------------------- /playground/class-components/src/utils.tsx: -------------------------------------------------------------------------------- 1 | export const getGetting = () => Hello; 2 | -------------------------------------------------------------------------------- /playground/ts-lib/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return

404

; 3 | } 4 | -------------------------------------------------------------------------------- /playground/ts-lib/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return

About page

; 3 | } 4 | -------------------------------------------------------------------------------- /playground/mdx/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mdx" { 2 | import { JSX } from "react"; 3 | export default () => JSX.Element; 4 | } 5 | -------------------------------------------------------------------------------- /playground/hmr/src/TitleWithExport.tsx: -------------------------------------------------------------------------------- 1 | export const framework = "React"; 2 | 3 | export const TitleWithExport = () =>

Vite + {framework}

; 4 | -------------------------------------------------------------------------------- /playground/react-18/src/TitleWithExport.tsx: -------------------------------------------------------------------------------- 1 | export const framework = "React"; 2 | 3 | export const TitleWithExport = () =>

Vite + {framework}

; 4 | -------------------------------------------------------------------------------- /playground/worker/src/worker-via-url.ts: -------------------------------------------------------------------------------- 1 | function printAlive(): void { 2 | console.log("Worker lives!"); 3 | } 4 | 5 | printAlive(); 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /playground/worker/src/worker-via-import.ts: -------------------------------------------------------------------------------- 1 | function printAlive(): void { 2 | console.log("Worker imported!"); 3 | } 4 | 5 | printAlive(); 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /playground/hmr/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/react-18/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/worker/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import { publish } from "@vitejs/release-scripts"; 2 | 3 | publish({ 4 | defaultPackage: "plugin-react-swc", 5 | getPkgDir: () => "dist", 6 | provenance: true, 7 | }); 8 | -------------------------------------------------------------------------------- /playground/shadow-export/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/class-components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/mdx/src/hello.mdx: -------------------------------------------------------------------------------- 1 | import { Counter } from './Counter.tsx' 2 | 3 | # Hello 4 | 5 | This text is written in Markdown. 6 | 7 | MDX allows Rich React components to be used directly in Markdown: 8 | -------------------------------------------------------------------------------- /playground/decorators/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react({ tsDecorators: true })], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/base-path/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | base: "/base-test/", 7 | }); 8 | -------------------------------------------------------------------------------- /playground/emotion/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react({ jsxImportSource: "@emotion/react" })], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/mdx/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import mdx from "@mdx-js/rollup"; 3 | import react from "@vitejs/plugin-react-swc"; 4 | 5 | export default defineConfig({ 6 | plugins: [mdx(), react()], 7 | }); 8 | -------------------------------------------------------------------------------- /playground/base-path/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const App = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /playground/ts-lib/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Home page

7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /playground/class-components/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { getGetting } from "./utils.tsx"; 3 | 4 | export class App extends Component { 5 | render() { 6 | return {getGetting()} World; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /playground/mdx/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const Counter = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /playground/ts-lib/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | optimizeDeps: { include: ["react-router-dom"] }, 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /playground/styled-components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [react({ plugins: [["@swc/plugin-styled-components", {}]] })], 6 | }); 7 | -------------------------------------------------------------------------------- /playground/mdx/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import Hello from "./hello.mdx"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/base-path/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/decorators/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/shadow-export/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/class-components/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/hmr/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /playground/ts-lib/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { Routes } from "generouted/react-router"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /playground/emotion/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /playground/react-18/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /playground/emotion-plugin/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.jsx"; 4 | import "./index.css"; 5 | 6 | createRoot(document.getElementById("root")).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /playground/styled-components/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /playground/emotion-plugin/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | react({ 7 | jsxImportSource: "@emotion/react", 8 | plugins: [["@swc/plugin-emotion", {}]], 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /playground/worker/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import MyWorker from "./worker-via-import.ts?worker&inline"; 3 | 4 | new MyWorker(); 5 | 6 | export const App = () => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ; 10 | }; 11 | -------------------------------------------------------------------------------- /playground/worker/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./App.tsx"; 4 | 5 | new Worker(new URL("./worker-via-url.ts", import.meta.url), { type: "module" }); 6 | 7 | createRoot(document.getElementById("root")!).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /playground/shadow-export/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | function App() { 4 | return
Shadow export
; 5 | } 6 | 7 | // For anyone reading this, don't do that 8 | // Use PascalCase for all components and export them directly without rename, 9 | // you're just making grep more complex. 10 | const withMemo = memo(App); 11 | export { withMemo as App }; 12 | -------------------------------------------------------------------------------- /playground/decorators/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass, Component } from "react"; 2 | 3 | function decorated(target: ComponentClass) { 4 | const original = target.prototype.render; 5 | 6 | target.prototype.render = () => { 7 | return
Hello {original()}
; 8 | }; 9 | } 10 | 11 | @decorated 12 | export class App extends Component { 13 | render() { 14 | return World; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/hmr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/ts-lib/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Outlet } from "react-router-dom"; 2 | 3 | export default function App() { 4 | return ( 5 |
6 |
7 | Home 8 | About 9 |
10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playground/mdx/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + MDX 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/react-18/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/ts-lib/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS lib 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/worker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + Worker 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/emotion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + Emotion 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/base-path/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + base path 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/decorators/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + decorators 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/emotion-plugin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + Emotion 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/shadow-export/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + shadow export 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/class-components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + class components 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/styled-components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + Styled Components 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/emotion/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.emotion:hover { 17 | filter: drop-shadow(0 0 2em #d26ac2aa); 18 | } 19 | 20 | .card { 21 | padding: 2em; 22 | } 23 | 24 | .read-the-docs { 25 | color: #888; 26 | } 27 | -------------------------------------------------------------------------------- /playground/hmr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-hmr", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ARCHIVED] @vitejs/plugin-react-swc [![npm](https://img.shields.io/npm/v/@vitejs/plugin-react-swc)](https://www.npmjs.com/package/@vitejs/plugin-react-swc) 2 | 3 | This repo has moved to [vitejs/vite-plugin-react:/packages/vite-plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc). 4 | 5 | ## Consistent components exports 6 | 7 | See . 8 | -------------------------------------------------------------------------------- /playground/emotion-plugin/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.emotion:hover { 17 | filter: drop-shadow(0 0 2em #d26ac2aa); 18 | } 19 | 20 | .card { 21 | padding: 2em; 22 | } 23 | 24 | .read-the-docs { 25 | color: #888; 26 | } 27 | -------------------------------------------------------------------------------- /playground/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-worker", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/base-path/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-base-test", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/react-18/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-react-18", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.3.1", 12 | "react-dom": "^18.3.1" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.3.18", 16 | "@types/react-dom": "^18.3.5", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/class-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "class-components", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/decorators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-decorators", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/styled-components/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.styled-components:hover { 17 | filter: drop-shadow(0 0 2em #db7093aa); 18 | } 19 | 20 | .card { 21 | padding: 2em; 22 | } 23 | 24 | .read-the-docs { 25 | color: #888; 26 | } 27 | -------------------------------------------------------------------------------- /playground/shadow-export/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-shadow-export", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^19.0.11", 16 | "@types/react-dom": "^19.0.4", 17 | "@vitejs/plugin-react-swc": "../../dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/mdx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-mdx", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0" 13 | }, 14 | "devDependencies": { 15 | "@mdx-js/rollup": "^3.1.0", 16 | "@types/react": "^19.0.11", 17 | "@types/react-dom": "^19.0.4", 18 | "@vitejs/plugin-react-swc": "../../dist" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/emotion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-emotion", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.14.0", 12 | "@emotion/styled": "^11.14.0", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^19.0.11", 18 | "@types/react-dom": "^19.0.4", 19 | "@vitejs/plugin-react-swc": "../../dist" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playground/ts-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-ts-lib", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@generouted/react-router": "^1.20.0", 12 | "generouted": "1.11.7", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0", 15 | "react-router-dom": "^7.3.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.0.11", 19 | "@types/react-dom": "^19.0.4", 20 | "@vitejs/plugin-react-swc": "../../dist" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playground/emotion-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-emotion-plugin", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.14.0", 12 | "@emotion/styled": "^11.14.0", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^19.0.11", 18 | "@types/react-dom": "^19.0.4", 19 | "@vitejs/plugin-react-swc": "../../dist", 20 | "@swc/plugin-emotion": "^9.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playground/emotion/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | display: flex; 21 | place-items: center; 22 | min-width: 320px; 23 | min-height: 100vh; 24 | } 25 | -------------------------------------------------------------------------------- /playground/emotion-plugin/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | display: flex; 21 | place-items: center; 22 | min-width: 320px; 23 | min-height: 100vh; 24 | } 25 | -------------------------------------------------------------------------------- /playground/styled-components/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | display: flex; 21 | place-items: center; 22 | min-width: 320px; 23 | min-height: 100vh; 24 | } 25 | -------------------------------------------------------------------------------- /playground/styled-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-styled-components", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0", 13 | "react-is": "^19.0.0", 14 | "styled-components": "^6.1.16" 15 | }, 16 | "devDependencies": { 17 | "@swc/plugin-styled-components": "^7.1.0", 18 | "@types/react": "^19.0.11", 19 | "@types/react-dom": "^19.0.4", 20 | "@types/styled-components": "^5.1.34", 21 | "@vitejs/plugin-react-swc": "../../dist" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/hmr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/mdx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: {} 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | cache: "pnpm" 20 | - run: pnpm install --frozen-lockfile --prefer-offline 21 | - uses: actions/cache@v4 22 | with: 23 | key: playwright-bin 24 | path: ~/.cache/ms-playwright 25 | - run: pnpm playwright install chromium 26 | - run: pnpm qa 27 | -------------------------------------------------------------------------------- /playground/base-path/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/react-18/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/shadow-export/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/class-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/styled-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "useUnknownInCatchVariables": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /playground/shadow-export/__tests__/shadow-export.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { setupDevServer, setupWaitForLogs } from "../../utils.ts"; 3 | 4 | test("Shadow export HMR", async ({ page }) => { 5 | const { testUrl, server, editFile } = await setupDevServer("shadow-export"); 6 | const waitForLogs = await setupWaitForLogs(page); 7 | await page.goto(testUrl); 8 | await waitForLogs("[vite] connected."); 9 | await expect(page.locator("body")).toHaveText("Shadow export"); 10 | 11 | editFile("src/App.tsx", ["Shadow export", "Shadow export updates!"]); 12 | await expect(page.locator("body")).toHaveText("Shadow export updates!"); 13 | 14 | await server.close(); 15 | }); 16 | -------------------------------------------------------------------------------- /playground/decorators/__tests__/decorators.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { setupDevServer, setupBuildAndPreview } from "../../utils.ts"; 3 | 4 | test("Decorators build", async ({ page }) => { 5 | const { testUrl, server } = await setupBuildAndPreview("decorators"); 6 | await page.goto(testUrl); 7 | 8 | await expect(page.locator("body")).toHaveText("Hello World"); 9 | 10 | await server.httpServer.close(); 11 | }); 12 | 13 | test("Decorators dev", async ({ page }) => { 14 | const { testUrl, server } = await setupDevServer("decorators"); 15 | await page.goto(testUrl); 16 | 17 | await expect(page.locator("body")).toHaveText("Hello World"); 18 | 19 | await server.close(); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | labels: ["dependencies"], 4 | rangeStrategy: "bump", 5 | packageRules: [ 6 | { depTypeList: ["peerDependencies"], enabled: false }, 7 | { 8 | "matchFileNames": ["**/react-18/**"], 9 | "ignoreDeps": ["react", "react-dom", "@types/react", "@types/react-dom"] 10 | }, 11 | { 12 | "matchDepTypes": ["action"], 13 | "excludePackagePrefixes": ["actions/", "github/"], 14 | "pinDigests": true, 15 | }, 16 | ], 17 | ignoreDeps: [ 18 | "generouted", // testing lib shipping JSX (new version ship transpiled JS) 19 | "prettier", // waiting for stable choice on ternaries 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /playground/base-path/__tests__/base-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { setupDevServer, setupWaitForLogs } from "../../utils.ts"; 3 | 4 | test("Base path HMR", async ({ page }) => { 5 | const { testUrl, server, editFile } = await setupDevServer("base-path"); 6 | const waitForLogs = await setupWaitForLogs(page); 7 | await page.goto(testUrl); 8 | 9 | const button = page.locator("button"); 10 | await button.click(); 11 | await expect(button).toHaveText("count is 1"); 12 | 13 | editFile("src/App.tsx", ["{count}", "{count}!"]); 14 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 15 | await expect(button).toHaveText("count is 1!"); 16 | 17 | await server.close(); 18 | }); 19 | -------------------------------------------------------------------------------- /playground/decorators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "experimentalDecorators": true, 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "useUnknownInCatchVariables": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /playground/emotion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "@emotion/react", 9 | "types": ["vite/client", "@emotion/react"], 10 | "noEmit": true, 11 | "isolatedModules": true, 12 | "skipLibCheck": true, 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "useUnknownInCatchVariables": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /playground/hmr/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /playground/react-18/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /playground/ts-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "jsx": "react-jsx", 8 | "types": ["vite/client"], 9 | "noEmit": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "useUnknownInCatchVariables": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/emotion-plugin/src/Button.jsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { css } from "@emotion/react"; 3 | import { useState } from "react"; 4 | 5 | // Ensure HMR of styled component alongside other components 6 | export const StyledCode = styled.code` 7 | color: #646cff; 8 | `; 9 | 10 | export const Button = ({ color }) => { 11 | const [count, setCount] = useState(0); 12 | 13 | return ( 14 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | environment: Release 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | registry-url: "https://registry.npmjs.org" 23 | cache: "pnpm" 24 | - run: pnpm install --frozen-lockfile --prefer-offline 25 | - run: pnpm build 26 | - run: pnpm tnode scripts/publish.ts ${{ github.ref_name }} 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | - uses: ArnaudBarre/github-release@v1 30 | -------------------------------------------------------------------------------- /playground/emotion/src/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { css } from "@emotion/react"; 3 | import { useState } from "react"; 4 | 5 | // Ensure HMR of styled component alongside other components 6 | export const StyledCode = styled.code` 7 | color: #646cff; 8 | `; 9 | 10 | export const Button = ({ color }: { color: string }) => { 11 | const [count, setCount] = useState(0); 12 | 13 | return ( 14 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { devices, type PlaywrightTestConfig } from "@playwright/test"; 3 | import fs from "fs-extra"; 4 | 5 | const tempDir = fileURLToPath(new URL("playground-temp", import.meta.url)); 6 | fs.ensureDirSync(tempDir); 7 | fs.emptyDirSync(tempDir); 8 | fs.copySync(fileURLToPath(new URL("playground", import.meta.url)), tempDir, { 9 | filter: (src) => 10 | !src.includes("/__tests__") && 11 | !src.includes("/.vite") && 12 | !src.includes("/dist"), 13 | }); 14 | 15 | const config: PlaywrightTestConfig = { 16 | forbidOnly: !!process.env["CI"], 17 | workers: 1, 18 | timeout: 10_000, 19 | reporter: process.env["CI"] ? "github" : "list", 20 | projects: [{ name: "chromium", use: devices["Desktop Chrome"] }], 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "scripts", 5 | "playwright.config.ts", 6 | "playground/utils.ts", 7 | "playground/*/__tests__" 8 | ], 9 | "compilerOptions": { 10 | /* Target node 22 */ 11 | "module": "ESNext", 12 | "lib": ["ES2023", "DOM"], 13 | "target": "ES2023", 14 | "skipLibCheck": true, 15 | 16 | /* Bundler mode */ 17 | "moduleResolution": "bundler", 18 | "allowImportingTsExtensions": true, 19 | "verbatimModuleSyntax": true, 20 | "noEmit": true, 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "useUnknownInCatchVariables": true, 28 | "noUncheckedSideEffectImports": true, 29 | "noPropertyAccessFromIndexSignature": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /playground/styled-components/src/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { useState } from "react"; 3 | 4 | // Ensure HMR of styled component alongside other components 5 | export const StyledCode = styled.code` 6 | color: palevioletred; 7 | `; 8 | 9 | const Button = styled.button` 10 | border-radius: 3px; 11 | padding: 0.5rem 1rem; 12 | color: white; 13 | background: transparent; 14 | border: 2px solid white; 15 | ${(props: { primary?: boolean }) => 16 | props.primary && 17 | css` 18 | background: white; 19 | color: black; 20 | `} 21 | `; 22 | 23 | export const Counter = ({ primary }: { primary?: boolean }) => { 24 | const [count, setCount] = useState(0); 25 | 26 | return ( 27 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /playground/emotion/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Button, StyledCode } from "./Button.tsx"; 3 | 4 | export const App = () => ( 5 |
6 |
7 | 8 | Vite logo 9 | 10 | 11 | Emotion logo 16 | 17 |
18 |
19 |
24 |

25 | Click on the Vite and Emotion logos to learn more 26 |

27 |
28 | ); 29 | -------------------------------------------------------------------------------- /playground/emotion-plugin/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Button, StyledCode } from "./Button.jsx"; 3 | 4 | export const App = () => ( 5 |
6 |
7 | 8 | Vite logo 9 | 10 | 11 | Emotion logo 16 | 17 |
18 |
19 |
24 |

25 | Click on the Vite and Emotion logos to learn more 26 |

27 |
28 | ); 29 | -------------------------------------------------------------------------------- /playground/class-components/__tests__/class-components.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { setupDevServer, setupWaitForLogs } from "../../utils.ts"; 3 | 4 | test("Class component HMR", async ({ page }) => { 5 | const { testUrl, server, editFile } = 6 | await setupDevServer("class-components"); 7 | const waitForLogs = await setupWaitForLogs(page); 8 | await page.goto(testUrl); 9 | 10 | await expect(page.locator("body")).toHaveText("Hello World"); 11 | editFile("src/App.tsx", ["World", "class components"]); 12 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 13 | await expect(page.locator("body")).toHaveText("Hello class components"); 14 | 15 | editFile("src/utils.tsx", ["Hello", "Hi"]); 16 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 17 | await expect(page.locator("body")).toHaveText("Hi class components"); 18 | 19 | await server.close(); 20 | }); 21 | -------------------------------------------------------------------------------- /playground/styled-components/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Counter, StyledCode } from "./Button.tsx"; 3 | 4 | export const App = () => ( 5 |
6 |
7 | 8 | Vite logo 9 | 10 | 11 | styled components logo 16 | 17 |
18 |
19 | 20 |

21 | Edit src/Button.tsx and save to test HMR 22 |

23 |
24 |

25 | Click on the Vite and Styled Components logos to learn more 26 |

27 |
28 | ); 29 | -------------------------------------------------------------------------------- /playground/ts-lib/__tests__/ts-lib.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { setupDevServer, setupBuildAndPreview } from "../../utils.ts"; 3 | 4 | test("TS lib build", async ({ page }) => { 5 | const { testUrl, server } = await setupBuildAndPreview("ts-lib"); 6 | await page.goto(testUrl); 7 | await expect(page.locator("main")).toHaveText("Home page"); 8 | 9 | await page.locator("a", { hasText: "About" }).click(); 10 | await expect(page.locator("main")).toHaveText("About page"); 11 | 12 | await server.httpServer.close(); 13 | }); 14 | 15 | test("TS lib dev", async ({ page }) => { 16 | const { testUrl, server } = await setupDevServer("ts-lib"); 17 | await page.goto(testUrl); 18 | await expect(page.locator("main")).toHaveText("Home page"); 19 | 20 | await page.locator("a", { hasText: "About" }).click(); 21 | await expect(page.locator("main")).toHaveText("About page"); 22 | 23 | await server.close(); 24 | }); 25 | -------------------------------------------------------------------------------- /playground/worker/__tests__/worker.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | } from "../../utils.ts"; 7 | 8 | test("Worker build", async ({ page }) => { 9 | const { testUrl, server } = await setupBuildAndPreview("worker"); 10 | const waitForLogs = await setupWaitForLogs(page); 11 | await page.goto(testUrl); 12 | await waitForLogs("Worker lives!", "Worker imported!"); 13 | 14 | await server.httpServer.close(); 15 | }); 16 | 17 | test("Worker HMR", async ({ page }) => { 18 | const { testUrl, server, editFile } = await setupDevServer("worker"); 19 | const waitForLogs = await setupWaitForLogs(page); 20 | await page.goto(testUrl); 21 | await waitForLogs("Worker lives!", "Worker imported!"); 22 | 23 | editFile("src/worker-via-url.ts", ["Worker lives!", "Worker updates!"]); 24 | await waitForLogs("Worker updates!"); 25 | 26 | await server.close(); 27 | }); 28 | -------------------------------------------------------------------------------- /playground/hmr/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import reactLogo from "./react.svg"; 3 | import "./App.css"; 4 | import { TitleWithExport, framework } from "./TitleWithExport.tsx"; 5 | 6 | export const App = () => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 |
11 |
12 | 13 | Vite logo 14 | 15 | 16 | React logo 17 | 18 |
19 | 20 |
21 | 22 |

23 | Edit src/App.tsx and save to test HMR 24 |

25 |
26 |

27 | Click on the Vite and {framework} logos to learn more 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /playground/react-18/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import reactLogo from "./react.svg"; 3 | import "./App.css"; 4 | import { TitleWithExport, framework } from "./TitleWithExport.tsx"; 5 | 6 | export const App = () => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 |
11 |
12 | 13 | Vite logo 14 | 15 | 16 | React logo 17 | 18 |
19 | 20 |
21 | 22 |

23 | Edit src/App.tsx and save to test HMR 24 |

25 |
26 |

27 | Click on the Vite and {framework} logos to learn more 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /playground/hmr/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/mdx/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Arnaud Barré (https://github.com/ArnaudBarre) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /playground/base-path/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/emotion/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/react-18/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/ts-lib/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/worker/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/decorators/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/emotion-plugin/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/shadow-export/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/class-components/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/styled-components/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vitejs/plugin-react-swc-monorepo", 3 | "version": "3.8.1", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "tnode scripts/bundle.ts --dev", 8 | "build": "tnode scripts/bundle.ts", 9 | "test": "playwright test", 10 | "prettier": "pnpm prettier-ci --write", 11 | "prettier-ci": "prettier --cache --ignore-path=.gitignore --check \"**/*.{js,jsx,ts,tsx,html,css,json,md,yml}\"", 12 | "qa": "tsc && pnpm prettier-ci && pnpm build && pnpm test", 13 | "release": "pnpm build && tnode scripts/release.ts" 14 | }, 15 | "packageManager": "pnpm@10.6.4", 16 | "dependencies": { 17 | "@swc/core": "^1.11.11" 18 | }, 19 | "peerDependencies": { 20 | "vite": "^4 || ^5 || ^6" 21 | }, 22 | "devDependencies": { 23 | "@arnaud-barre/tnode": "^0.24.0", 24 | "@playwright/test": "^1.51.1", 25 | "@types/fs-extra": "^11.0.4", 26 | "@types/node": "22.13.10", 27 | "@vitejs/release-scripts": "^1.3.3", 28 | "esbuild": "^0.25.1", 29 | "fs-extra": "^11.3.0", 30 | "picocolors": "^1.1.1", 31 | "prettier": "^3.0.3", 32 | "typescript": "^5.8.2", 33 | "vite": "^6.2.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "node:fs"; 2 | import { release } from "@vitejs/release-scripts"; 3 | import colors from "picocolors"; 4 | 5 | const changelog = readFileSync("CHANGELOG.md", "utf-8"); 6 | 7 | release({ 8 | repo: "vite-plugin-react-swc", 9 | packages: ["plugin-react-swc"], 10 | toTag: (_, version) => `v${version}`, 11 | logChangelog: () => { 12 | if (!changelog.includes("## Unreleased")) { 13 | throw new Error("Can't find '## Unreleased' section in CHANGELOG.md"); 14 | } 15 | const index = changelog.indexOf("## Unreleased") + 13; 16 | console.log( 17 | colors.dim( 18 | changelog.slice(index, changelog.indexOf("\n## ", index)).trim(), 19 | ), 20 | ); 21 | }, 22 | generateChangelog: (_, version) => { 23 | console.log(colors.dim("Write package.json & CHANGELOG.md")); 24 | const pkg = JSON.parse(readFileSync("package.json", "utf-8")); 25 | pkg.version = version; 26 | writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`); 27 | writeFileSync( 28 | "CHANGELOG.md", 29 | changelog.replace("## Unreleased", `## Unreleased\n\n## ${version}`), 30 | ); 31 | }, 32 | getPkgDir: () => "dist", 33 | }); 34 | -------------------------------------------------------------------------------- /playground/mdx/__tests__/mdx.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | } from "../../utils.ts"; 7 | 8 | test("MDX build", async ({ page }) => { 9 | const { testUrl, server } = await setupBuildAndPreview("mdx"); 10 | await page.goto(testUrl); 11 | await expect(page.getByRole("heading", { name: "Hello" })).toBeVisible(); 12 | await server.httpServer.close(); 13 | }); 14 | 15 | test("MDX HMR", async ({ page }) => { 16 | const { testUrl, server, editFile } = await setupDevServer("mdx"); 17 | const waitForLogs = await setupWaitForLogs(page); 18 | await page.goto(testUrl); 19 | await waitForLogs("[vite] connected."); 20 | 21 | await expect(page.getByRole("heading", { name: "Hello" })).toBeVisible(); 22 | 23 | editFile("src/Counter.tsx", ["{count}", "{count}!"]); 24 | await waitForLogs("[vite] hot updated: /src/Counter.tsx"); 25 | const button = await page.locator("button"); 26 | await button.click(); 27 | await expect(button).toHaveText("count is 1!"); 28 | 29 | editFile("src/hello.mdx", ["Hello", "Hello world"]); 30 | await waitForLogs("[vite] hot updated: /src/hello.mdx"); 31 | await expect( 32 | page.getByRole("heading", { name: "Hello world" }), 33 | ).toBeVisible(); 34 | await expect(button).toHaveText("count is 1!"); 35 | 36 | await server.close(); 37 | }); 38 | -------------------------------------------------------------------------------- /playground/hmr/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /playground/react-18/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /playground/emotion/__tests__/emotion.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | expectColor, 7 | } from "../../utils.ts"; 8 | 9 | test("Emotion build", async ({ page }) => { 10 | const { testUrl, server } = await setupBuildAndPreview("emotion"); 11 | await page.goto(testUrl); 12 | 13 | const button = page.locator("button"); 14 | await button.hover(); 15 | await expectColor(button, "color", "#646cff"); 16 | 17 | await button.click(); 18 | await expect(button).toHaveText("count is 1"); 19 | 20 | const code = page.locator("code"); 21 | await expectColor(code, "color", "#646cff"); 22 | 23 | await server.httpServer.close(); 24 | }); 25 | 26 | test("Emotion HMR", async ({ page }) => { 27 | const { testUrl, server, editFile } = await setupDevServer("emotion"); 28 | const waitForLogs = await setupWaitForLogs(page); 29 | await page.goto(testUrl); 30 | await waitForLogs("[vite] connected."); 31 | 32 | const button = page.locator("button"); 33 | await button.hover(); 34 | await expectColor(button, "color", "#646cff"); 35 | 36 | await button.click(); 37 | await expect(button).toHaveText("count is 1"); 38 | 39 | const code = page.locator("code"); 40 | await expectColor(code, "color", "#646cff"); 41 | 42 | editFile("src/Button.tsx", [ 43 | "background-color: #d26ac2;", 44 | "background-color: #646cff;", 45 | ]); 46 | await waitForLogs("[vite] hot updated: /src/Button.tsx"); 47 | await expect(button).toHaveText("count is 1"); 48 | await expectColor(button, "backgroundColor", "#646cff"); 49 | 50 | editFile("src/App.tsx", ['color="#646cff"', 'color="#d26ac2"']); 51 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 52 | await expect(button).toHaveText("count is 1"); 53 | await expectColor(button, "color", "#d26ac2"); 54 | 55 | editFile("src/Button.tsx", ["color: #646cff;", "color: #d26ac2;"]); 56 | await waitForLogs("[vite] hot updated: /src/Button.tsx"); 57 | await expect(button).toHaveText("count is 1"); 58 | await expectColor(code, "color", "#d26ac2"); 59 | 60 | await server.close(); 61 | }); 62 | -------------------------------------------------------------------------------- /playground/emotion-plugin/__tests__/emotion-plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | expectColor, 7 | } from "../../utils.ts"; 8 | 9 | test("Emotion plugin build", async ({ page }) => { 10 | const { testUrl, server } = await setupBuildAndPreview("emotion-plugin"); 11 | await page.goto(testUrl); 12 | 13 | const button = page.locator("button"); 14 | await button.hover(); 15 | await expectColor(button, "color", "#646cff"); 16 | 17 | await button.click(); 18 | await expect(button).toHaveText("count is 1"); 19 | 20 | const code = page.locator("code"); 21 | await expectColor(code, "color", "#646cff"); 22 | 23 | await server.httpServer.close(); 24 | }); 25 | 26 | test("Emotion plugin HMR", async ({ page }) => { 27 | const { testUrl, server, editFile } = await setupDevServer("emotion-plugin"); 28 | const waitForLogs = await setupWaitForLogs(page); 29 | await page.goto(testUrl); 30 | await waitForLogs("[vite] connected."); 31 | 32 | const button = page.locator("button"); 33 | await button.hover(); 34 | await expectColor(button, "color", "#646cff"); 35 | 36 | await button.click(); 37 | await expect(button).toHaveText("count is 1"); 38 | 39 | const code = page.locator("code"); 40 | await expectColor(code, "color", "#646cff"); 41 | 42 | editFile("src/Button.jsx", [ 43 | "background-color: #d26ac2;", 44 | "background-color: #646cff;", 45 | ]); 46 | await waitForLogs("[vite] hot updated: /src/Button.jsx"); 47 | await expect(button).toHaveText("count is 1"); 48 | await expectColor(button, "backgroundColor", "#646cff"); 49 | 50 | editFile("src/App.jsx", ['color="#646cff"', 'color="#d26ac2"']); 51 | await waitForLogs("[vite] hot updated: /src/App.jsx"); 52 | await expect(button).toHaveText("count is 1"); 53 | await expectColor(button, "color", "#d26ac2"); 54 | 55 | editFile("src/Button.jsx", ["color: #646cff;", "color: #d26ac2;"]); 56 | await waitForLogs("[vite] hot updated: /src/Button.jsx"); 57 | await expect(button).toHaveText("count is 1"); 58 | await expectColor(code, "color", "#d26ac2"); 59 | 60 | await server.close(); 61 | }); 62 | -------------------------------------------------------------------------------- /playground/styled-components/__tests__/styled-components.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | expectColor, 7 | } from "../../utils.ts"; 8 | 9 | test("styled-components build", async ({ page }) => { 10 | const { testUrl, server } = await setupBuildAndPreview("styled-components"); 11 | await page.goto(testUrl); 12 | 13 | const button = page.locator("button"); 14 | await button.click(); 15 | await expect(button).toHaveText("count is 1"); 16 | await expectColor(button, "color", "#ffffff"); 17 | 18 | const code = page.locator("code"); 19 | await expectColor(code, "color", "#db7093"); 20 | await expect(code).toHaveClass(/Button__StyledCode/); 21 | 22 | await server.httpServer.close(); 23 | }); 24 | 25 | test("styled-components HMR", async ({ page }) => { 26 | const { testUrl, server, editFile } = 27 | await setupDevServer("styled-components"); 28 | const waitForLogs = await setupWaitForLogs(page); 29 | await page.goto(testUrl); 30 | await waitForLogs("[vite] connected."); 31 | 32 | const button = page.locator("button"); 33 | await expect(button).toHaveText("count is 0", { timeout: 30000 }); 34 | await expectColor(button, "color", "#ffffff"); 35 | await button.click(); 36 | await expect(button).toHaveText("count is 1"); 37 | 38 | const code = page.locator("code"); 39 | await expectColor(code, "color", "#db7093"); 40 | await expect(code).toHaveClass(/Button__StyledCode/); 41 | 42 | editFile("src/App.tsx", ["", ""]); 43 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 44 | await expect(button).toHaveText("count is 1"); 45 | await expectColor(button, "color", "#000000"); 46 | 47 | editFile("src/Button.tsx", ["color: black;", "color: palevioletred;"]); 48 | await waitForLogs("[vite] hot updated: /src/Button.tsx"); 49 | await expect(button).toHaveText("count is 1"); 50 | await expectColor(button, "color", "#db7093"); 51 | 52 | editFile("src/Button.tsx", ["color: palevioletred;", "color: white;"]); 53 | await waitForLogs("[vite] hot updated: /src/Button.tsx"); 54 | await expect(button).toHaveText("count is 1"); 55 | await expectColor(code, "color", "#ffffff"); 56 | 57 | await server.close(); 58 | }); 59 | -------------------------------------------------------------------------------- /scripts/bundle.ts: -------------------------------------------------------------------------------- 1 | import { rmSync, writeFileSync, copyFileSync } from "node:fs"; 2 | import { execSync } from "node:child_process"; 3 | import { build, type BuildOptions, context } from "esbuild"; 4 | 5 | import packageJSON from "../package.json"; 6 | 7 | const dev = process.argv.includes("--dev"); 8 | 9 | rmSync("dist", { force: true, recursive: true }); 10 | 11 | const serverOptions: BuildOptions = { 12 | bundle: true, 13 | platform: "node", 14 | target: "node14", 15 | legalComments: "inline", 16 | external: Object.keys(packageJSON.peerDependencies).concat( 17 | Object.keys(packageJSON.dependencies), 18 | ), 19 | }; 20 | 21 | const buildOrWatch = async (options: BuildOptions) => { 22 | if (!dev) return build(options); 23 | const ctx = await context(options); 24 | await ctx.watch(); 25 | await ctx.rebuild(); 26 | }; 27 | 28 | Promise.all([ 29 | buildOrWatch({ 30 | entryPoints: ["src/refresh-runtime.js"], 31 | outdir: "dist", 32 | platform: "browser", 33 | format: "esm", 34 | target: "safari13", 35 | legalComments: "inline", 36 | }), 37 | buildOrWatch({ 38 | ...serverOptions, 39 | stdin: { 40 | contents: `import react from "./src"; 41 | module.exports = react; 42 | // For backward compatibility with the first broken version 43 | module.exports.default = react;`, 44 | resolveDir: ".", 45 | }, 46 | outfile: "dist/index.cjs", 47 | logOverride: { "empty-import-meta": "silent" }, 48 | }), 49 | buildOrWatch({ 50 | ...serverOptions, 51 | entryPoints: ["src/index.ts"], 52 | format: "esm", 53 | outfile: "dist/index.mjs", 54 | }), 55 | ]).then(() => { 56 | copyFileSync("LICENSE", "dist/LICENSE"); 57 | copyFileSync("README.md", "dist/README.md"); 58 | 59 | execSync( 60 | "tsc src/index.ts --declaration --isolatedDeclarations --noCheck --emitDeclarationOnly --outDir dist --target es2020 --module es2020 --moduleResolution bundler", 61 | { stdio: "inherit" }, 62 | ); 63 | 64 | writeFileSync( 65 | "dist/package.json", 66 | JSON.stringify( 67 | { 68 | name: "@vitejs/plugin-react-swc", 69 | description: "Speed up your Vite dev server with SWC", 70 | version: packageJSON.version, 71 | author: "Arnaud Barré (https://github.com/ArnaudBarre)", 72 | license: "MIT", 73 | repository: "github:vitejs/vite-plugin-react-swc", 74 | type: "module", 75 | main: "index.cjs", 76 | types: "index.d.ts", 77 | module: "index.mjs", 78 | exports: { 79 | ".": { 80 | types: "./index.d.ts", 81 | require: "./index.cjs", 82 | import: "./index.mjs", 83 | }, 84 | }, 85 | keywords: [ 86 | "vite", 87 | "vite-plugin", 88 | "react", 89 | "swc", 90 | "react-refresh", 91 | "fast refresh", 92 | ], 93 | peerDependencies: packageJSON.peerDependencies, 94 | dependencies: packageJSON.dependencies, 95 | }, 96 | null, 97 | 2, 98 | ), 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /playground/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect, type Locator, type Page } from "@playwright/test"; 2 | import { 3 | createServer, 4 | loadConfigFromFile, 5 | mergeConfig, 6 | preview, 7 | build, 8 | } from "vite"; 9 | import { readFileSync, writeFileSync } from "fs"; 10 | 11 | export const setupWaitForLogs = async (page: Page) => { 12 | let logs: string[] = []; 13 | page.on("console", (log) => { 14 | logs.push(log.text()); 15 | }); 16 | return (...messages: (string | RegExp)[]) => 17 | expect 18 | .poll(() => { 19 | if ( 20 | messages.every((m) => 21 | typeof m === "string" 22 | ? logs.includes(m) 23 | : logs.some((l) => m.test(l)), 24 | ) 25 | ) { 26 | logs = []; 27 | return true; 28 | } 29 | return logs; 30 | }) 31 | .toBe(true); 32 | }; 33 | 34 | let port = 5173; 35 | export const setupDevServer = async (name: string) => { 36 | process.env["NODE_ENV"] = "development"; 37 | const root = `playground-temp/${name}`; 38 | const res = await loadConfigFromFile( 39 | { command: "serve", mode: "development" }, 40 | undefined, 41 | root, 42 | ); 43 | const testConfig = mergeConfig(res!.config, { 44 | root, 45 | logLevel: "silent", 46 | configFile: false, 47 | server: { port: port++ }, 48 | }); 49 | const server = await (await createServer(testConfig)).listen(); 50 | return { 51 | testUrl: `http://localhost:${server.config.server.port}${server.config.base}`, 52 | server, 53 | editFile: ( 54 | name: string, 55 | ...replacements: [searchValue: string, replaceValue: string][] 56 | ) => { 57 | const path = `${root}/${name}`; 58 | let content = readFileSync(path, "utf-8"); 59 | for (let [search, replace] of replacements) { 60 | if (!content.includes(search)) { 61 | throw new Error(`'${search}' not found in ${name}`); 62 | } 63 | content = content.replace(search, replace); 64 | } 65 | writeFileSync(path, content); 66 | }, 67 | }; 68 | }; 69 | 70 | export const setupBuildAndPreview = async (name: string) => { 71 | process.env["NODE_ENV"] = "production"; 72 | const root = `playground-temp/${name}`; 73 | const res = await loadConfigFromFile( 74 | { command: "build", mode: "production" }, 75 | undefined, 76 | root, 77 | ); 78 | const testConfig = mergeConfig( 79 | { root, logLevel: "silent", configFile: false, preview: { port: port++ } }, 80 | res!.config, 81 | ); 82 | await build(testConfig); 83 | const server = await preview(testConfig); 84 | return { 85 | testUrl: server.resolvedUrls!.local[0], 86 | server, 87 | }; 88 | }; 89 | 90 | export const expectColor = async ( 91 | locator: Locator, 92 | property: "color" | "backgroundColor", 93 | color: string, 94 | ) => { 95 | await expect 96 | .poll(async () => 97 | rgbToHex( 98 | await locator.evaluate( 99 | (el, prop) => getComputedStyle(el)[prop], 100 | property, 101 | ), 102 | ), 103 | ) 104 | .toBe(color); 105 | }; 106 | 107 | const rgbToHex = (rgb: string): string => { 108 | const [_, rs, gs, bs] = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)!; 109 | return ( 110 | "#" + 111 | componentToHex(parseInt(rs, 10)) + 112 | componentToHex(parseInt(gs, 10)) + 113 | componentToHex(parseInt(bs, 10)) 114 | ); 115 | }; 116 | 117 | const componentToHex = (c: number): string => { 118 | const hex = c.toString(16); 119 | return hex.length === 1 ? "0" + hex : hex; 120 | }; 121 | -------------------------------------------------------------------------------- /playground/hmr/__tests__/hmr.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | } from "../../utils.ts"; 7 | 8 | test("Default build", async ({ page }) => { 9 | const { testUrl, server } = await setupBuildAndPreview("hmr"); 10 | await page.goto(testUrl); 11 | 12 | await page.click("button"); 13 | await expect(page.locator("button")).toHaveText("count is 1"); 14 | 15 | await server.httpServer.close(); 16 | }); 17 | 18 | test("HMR invalidate", async ({ page }) => { 19 | const { testUrl, server, editFile } = await setupDevServer("hmr"); 20 | const waitForLogs = await setupWaitForLogs(page); 21 | await page.goto(testUrl); 22 | await waitForLogs("[vite] connected."); 23 | 24 | await page.click("button"); 25 | await expect(page.locator("button")).toHaveText("count is 1"); 26 | 27 | editFile("src/App.tsx", ["{count}", "{count}!"]); 28 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 29 | await expect(page.locator("button")).toHaveText("count is 1!"); 30 | 31 | // Edit component 32 | editFile("src/TitleWithExport.tsx", ["Vite +", "Vite *"]); 33 | await waitForLogs("[vite] hot updated: /src/TitleWithExport.tsx"); 34 | 35 | // Edit export 36 | editFile("src/TitleWithExport.tsx", ["React", "React!"]); 37 | await waitForLogs( 38 | '[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports', 39 | "[vite] hot updated: /src/App.tsx", 40 | ); 41 | await expect(page.locator("h1")).toHaveText("Vite * React!"); 42 | 43 | // Add non-component export 44 | editFile("src/TitleWithExport.tsx", [ 45 | 'React!";', 46 | 'React!";\nexport const useless = 3;', 47 | ]); 48 | await waitForLogs( 49 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (new export)", 50 | "[vite] hot updated: /src/App.tsx", 51 | ); 52 | 53 | // Add component export 54 | editFile("src/TitleWithExport.tsx", [ 55 | ";", 56 | ";\nexport const Title2 = () =>

Title2

;", 57 | ]); 58 | await waitForLogs("[vite] hot updated: /src/TitleWithExport.tsx"); 59 | 60 | // Import new component 61 | editFile( 62 | "src/App.tsx", 63 | ["import { TitleWithExport", "import { TitleWithExport, Title2"], 64 | ["", " "], 65 | ); 66 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 67 | await expect(page.locator("h2")).toHaveText("Title2"); 68 | 69 | // Remove component export 70 | editFile("src/TitleWithExport.tsx", [ 71 | "\nexport const Title2 = () =>

Title2

;", 72 | "", 73 | ]); 74 | await waitForLogs( 75 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (export removed)", 76 | "[vite] hot updated: /src/App.tsx", 77 | /Failed to reload \/src\/App\.tsx. This could be due to syntax errors or importing non-existent modules\. \(see errors above\)$/, 78 | ); 79 | 80 | // Remove usage from App 81 | editFile( 82 | "src/App.tsx", 83 | ["import { TitleWithExport, Title2", "import { TitleWithExport"], 84 | [" ", ""], 85 | ); 86 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 87 | await expect(page.locator("button")).toHaveText("count is 1!"); 88 | 89 | // Remove useless export 90 | editFile("src/TitleWithExport.tsx", ["\nexport const useless = 3;", ""]); 91 | await waitForLogs( 92 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (export removed)", 93 | "[vite] hot updated: /src/App.tsx", 94 | ); 95 | 96 | await server.close(); 97 | }); 98 | -------------------------------------------------------------------------------- /playground/react-18/__tests__/react-18.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { 3 | setupDevServer, 4 | setupBuildAndPreview, 5 | setupWaitForLogs, 6 | } from "../../utils.ts"; 7 | 8 | test("Default build", async ({ page }) => { 9 | const { testUrl, server } = await setupBuildAndPreview("react-18"); 10 | await page.goto(testUrl); 11 | 12 | await page.click("button"); 13 | await expect(page.locator("button")).toHaveText("count is 1"); 14 | 15 | await server.httpServer.close(); 16 | }); 17 | 18 | test("HMR invalidate", async ({ page }) => { 19 | const { testUrl, server, editFile } = await setupDevServer("react-18"); 20 | const waitForLogs = await setupWaitForLogs(page); 21 | await page.goto(testUrl); 22 | await waitForLogs("[vite] connected."); 23 | 24 | await page.click("button"); 25 | await expect(page.locator("button")).toHaveText("count is 1"); 26 | 27 | editFile("src/App.tsx", ["{count}", "{count}!"]); 28 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 29 | await expect(page.locator("button")).toHaveText("count is 1!"); 30 | 31 | // Edit component 32 | editFile("src/TitleWithExport.tsx", ["Vite +", "Vite *"]); 33 | await waitForLogs("[vite] hot updated: /src/TitleWithExport.tsx"); 34 | 35 | // Edit export 36 | editFile("src/TitleWithExport.tsx", ["React", "React!"]); 37 | await waitForLogs( 38 | '[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh ("framework" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports', 39 | "[vite] hot updated: /src/App.tsx", 40 | ); 41 | await expect(page.locator("h1")).toHaveText("Vite * React!"); 42 | 43 | // Add non-component export 44 | editFile("src/TitleWithExport.tsx", [ 45 | 'React!";', 46 | 'React!";\nexport const useless = 3;', 47 | ]); 48 | await waitForLogs( 49 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (new export)", 50 | "[vite] hot updated: /src/App.tsx", 51 | ); 52 | 53 | // Add component export 54 | editFile("src/TitleWithExport.tsx", [ 55 | ";", 56 | ";\nexport const Title2 = () =>

Title2

;", 57 | ]); 58 | await waitForLogs("[vite] hot updated: /src/TitleWithExport.tsx"); 59 | 60 | // Import new component 61 | editFile( 62 | "src/App.tsx", 63 | ["import { TitleWithExport", "import { TitleWithExport, Title2"], 64 | ["", " "], 65 | ); 66 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 67 | await expect(page.locator("h2")).toHaveText("Title2"); 68 | 69 | // Remove component export 70 | editFile("src/TitleWithExport.tsx", [ 71 | "\nexport const Title2 = () =>

Title2

;", 72 | "", 73 | ]); 74 | await waitForLogs( 75 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (export removed)", 76 | "[vite] hot updated: /src/App.tsx", 77 | /Failed to reload \/src\/App\.tsx. This could be due to syntax errors or importing non-existent modules\. \(see errors above\)$/, 78 | ); 79 | 80 | // Remove usage from App 81 | editFile( 82 | "src/App.tsx", 83 | ["import { TitleWithExport, Title2", "import { TitleWithExport"], 84 | [" ", ""], 85 | ); 86 | await waitForLogs("[vite] hot updated: /src/App.tsx"); 87 | await expect(page.locator("button")).toHaveText("count is 1!"); 88 | 89 | // Remove useless export 90 | editFile("src/TitleWithExport.tsx", ["\nexport const useless = 3;", ""]); 91 | await waitForLogs( 92 | "[vite] invalidate /src/TitleWithExport.tsx: Could not Fast Refresh (export removed)", 93 | "[vite] hot updated: /src/App.tsx", 94 | ); 95 | 96 | await server.close(); 97 | }); 98 | -------------------------------------------------------------------------------- /playground/hmr/src/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/react-18/src/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 3.8.1 6 | 7 | ### Remove WebContainers warning [#268](https://github.com/vitejs/vite-plugin-react-swc/pull/268) 8 | 9 | SWC is now supported in WebContainers 🎉 10 | 11 | ## 3.8.0 12 | 13 | ### Add useAtYourOwnRisk_mutateSwcOptions option 14 | 15 | The future of Vite is with OXC, and from the beginning this was a design choice to not exposed too many specialties from SWC so that Vite React users can move to another transformer later. 16 | Also debugging why some specific version of decorators with some other unstable/legacy feature doesn't work is not fun, so we won't provide support for it, hence the name `useAtYourOwnRisk`. 17 | 18 | ```ts 19 | react({ 20 | useAtYourOwnRisk_mutateSwcOptions(options) { 21 | options.jsc.parser.decorators = true; 22 | options.jsc.transform.decoratorVersion = "2022-03"; 23 | }, 24 | }); 25 | ``` 26 | 27 | ## 3.7.2 28 | 29 | ### Add Vite 6 to peerDependencies range [#207](https://github.com/vitejs/vite-plugin-react-swc/pull/207) 30 | 31 | Thanks @RobinTail 32 | 33 | ### Revert throw when refresh runtime is loaded twice [#237](https://github.com/vitejs/vite-plugin-react-swc/issues/237) 34 | 35 | Revert the throw when refresh runtime is loaded twice to enable usage in micro frontend apps. This was added to help fix setup usage, and this is not worth an annoying warning for others or a config parameter. 36 | 37 | This revert was done in the Babel plugin last year and I didn't port it back. 38 | 39 | ## 3.7.1 40 | 41 | Ignore directive sourcemap error [#231](https://github.com/vitejs/vite-plugin-react-swc/issues/231) 42 | 43 | ## 3.7.0 44 | 45 | ### Support HMR for class components 46 | 47 | This is a long overdue and should fix some issues people had with HMR when migrating from CRA. 48 | 49 | ## 3.6.0 50 | 51 | ### Add parserConfig option 52 | 53 | This will unlock to use the plugin in some use cases where the original source code is not in TS. Using this option to keep using JSX inside `.js` files is highly discouraged and can be removed in any future version. 54 | 55 | ## 3.5.0 56 | 57 | ### Update peer dependency range to target Vite 5 58 | 59 | There were no breaking change that impacted this plugin, so any combination of React plugins and Vite core version will work. 60 | 61 | ### Align jsx runtime for optimized dependencies 62 | 63 | This will only affect people using internal libraries that contains untranspiled JSX. This change aligns the optimizer with the source code and avoid issues when the published source don't have `React` in the scope. 64 | 65 | Reminder: While being partially supported in Vite, publishing TS & JSX outside of internal libraries is highly discouraged. 66 | 67 | ## 3.4.1 68 | 69 | ### Add support for `.mts` (fixes [#161](https://github.com/vitejs/vite-plugin-react-swc/issues/161)) 70 | 71 | Using CJS in source code will not work in Vite (and will never be supported), so this is better to only use `.ts`. 72 | 73 | But to better align with [Vite core defaults](https://vitejs.dev/config/shared-options.html#resolve-extensions), `.mts` extension will now be processed like `.ts`. This maybe reverted in a future major. 74 | 75 | ## 3.4.0 76 | 77 | - Add `devTarget` option (fixes [#141](https://github.com/vitejs/vite-plugin-react-swc/issues/141)) 78 | - Disable Fast Refresh based on `config.server.hmr === false` instead of `process.env.TEST` 79 | - Warn when plugin is in WebContainers (see [#118](https://github.com/vitejs/vite-plugin-react-swc/issues/118)) 80 | - Better invalidation message when an export is added & fix HMR for export of nullish values ([#143](https://github.com/vitejs/vite-plugin-react-swc/issues/143)) 81 | 82 | ## 3.3.2 83 | 84 | - Support [Vitest deps.experimentalOptimizer](https://vitest.dev/config/#deps-experimentaloptimizer) ([#115](https://github.com/vitejs/vite-plugin-react-swc/pull/115)) 85 | 86 | ## 3.3.1 87 | 88 | - Add `type: module` to package.json ([#101](https://github.com/vitejs/vite-plugin-react-swc/pull/101)). Because the library already publish `.cjs` & `.mjs` files, the only change is for typing when using the node16 module resolution (fixes [#95](https://github.com/vitejs/vite-plugin-react-swc/issues/95)) 89 | - Throw an error when the MDX plugin is after this one ([#100](https://github.com/vitejs/vite-plugin-react-swc/pull/100)). This is an expected breaking change added in `3.2.0` and this should people that were using both plugins before this version to migrate. 90 | 91 | ## 3.3.0 92 | 93 | - Support TS/JSX in node_modules to help the community experiment with it. Note that for now this not supported by TS and errors from these files cannot be silenced if the user is using a stricter configuration than the library author: https://github.com/microsoft/TypeScript/issues/30511. I advise to use it only for internal libraries for now (fixes #53) 94 | - Silence `"use client"` warning when building library like `@tanstack/react-query` 95 | - Fix fast refresh issue when exporting a component with a name that shadow another local component 96 | 97 | This release goes in hand with the upcoming Vite 4.3 release focusing on performances: 98 | 99 | - Move resolve of runtime code into a "pre" plugin ([#79](https://github.com/vitejs/vite-plugin-react-swc/pull/79)) 100 | - Wrap dynamic import to speedup analysis ([#80](https://github.com/vitejs/vite-plugin-react-swc/pull/80)) 101 | 102 | ## 3.2.0 103 | 104 | - Support HMR for MDX (fixes #52) 105 | - Fix: when using plugins, apply SWC before esbuild so that automatic runtime is respected for JSX (fixes #56) 106 | - Fix: use jsxImportSource in optimizeDeps 107 | 108 | ## 3.1.0 109 | 110 | - Support plugins via the new `plugins` options 111 | - Support TypeScript decorators via the new `tsDecorators` option. This requires `experimentalDecorators` in tsconfig. 112 | - Fix HMR for styled components exported alongside other components 113 | - Update embedded refresh runtime to 0.14 (fixes [#46](https://github.com/vitejs/vite-plugin-react-swc/issues/46)) 114 | 115 | ## 3.0.1 116 | 117 | - Support Emotion via the new `jsxImportSource` option (fixes [#25](https://github.com/vitejs/vite-plugin-react-swc/issues/25)) 118 | 119 | To use it with Emotion, update your config to: 120 | 121 | ```ts 122 | export default defineConfig({ 123 | plugins: [react({ jsxImportSource: "@emotion/react" })], 124 | }); 125 | ``` 126 | 127 | - Fix HMR when using Vite `base` option (fixes [#18](https://github.com/vitejs/vite-plugin-react-swc/issues/18)) 128 | - Fix usage with workers (fixes [#23](https://github.com/vitejs/vite-plugin-react-swc/issues/23)) 129 | - Fix usage with `Vite Ruby` and `Laravel Vite` ([#20](https://github.com/vitejs/vite-plugin-react-swc/pull/20)) 130 | - Fix plugin default export when using commonjs (fixes [#14](https://github.com/vitejs/vite-plugin-react-swc/issues/14)) 131 | 132 | ## 3.0.0 133 | 134 | This is plugin is now stable! 🎉 135 | 136 | To migrate from `vite-plugin-swc-react-refresh`, see the `3.0.0-beta.0` changelog. 137 | 138 | ## 3.0.0-beta.2 139 | 140 | - breaking: update plugin name to `vite:react-swc` to match official plugins naming 141 | - fix: don't add React Refresh wrapper for SSR transform (fixes [#11](https://github.com/vitejs/vite-plugin-react-swc/issues/11)) 142 | 143 | ## 3.0.0-beta.1 144 | 145 | Fix package.json exports fields 146 | 147 | ## 3.0.0-beta.0 148 | 149 | This is the first beta version of the official plugin for using [SWC](https://swc.rs/) with React in Vite! 150 | 151 | Some breaking changes have been made to make the plugin closer to the Babel one while keeping the smallest API surface possible to reduce bugs, encourage future-proof compilation output and allow easier opt-in into future perf improvements (caching, move to other native toolchain, ...): 152 | 153 | - Automatically enable automatic JSX runtime. "classic" runtime is not supported 154 | - Skip transformation for `.js` files 155 | - Enforce [useDefineForClassFields](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier) 156 | - Don't pass `esbuild.define` config option to SWC. You can use the [top level define option](https://vitejs.dev/config/shared-options.html#define) instead 157 | - Use default export 158 | 159 | To migrate, change your config to: 160 | 161 | ```js 162 | import { defineConfig } from "vite"; 163 | import react from "@vitejs/plugin-react-swc"; 164 | 165 | export default defineConfig({ 166 | plugins: [react()], 167 | }); 168 | ``` 169 | 170 | This new release also include a runtime check for React refresh boundaries. When the conditions are not met (most of the time, exporting React components alongside functions or constant), the module is invalidated with a warning message to help you catch issues while keeping you page up-to date with code changes. 171 | 172 | ## 2.2.1 173 | 174 | Skip react-refresh on SSR (Fixes [#2](https://github.com/vitejs/vite-plugin-react-swc/issues/2)) 175 | 176 | ## 2.2.0 177 | 178 | - Always provide parser options to fix issue with `.jsx` imports. Relying on file extension for this is more buggy [than I though](https://github.com/swc-project/swc/issues/3297) 179 | - Extract line and column in SWC errors to make overlay filename clickable 180 | - Fix plugin name (`react-refresh` -> `swc-react-refresh`) 181 | 182 | ## 2.1.0 183 | 184 | Add source maps support 185 | 186 | ## 2.0.3 187 | 188 | Include `react/jsx-dev-runtime` for dependencies optimisation when using automatic runtime. 189 | 190 | ## 2.0.2 191 | 192 | Unpinned `@swc/core` to get new features (like TS instantiation expression) despite a [30mb bump of bundle size](https://github.com/swc-project/swc/issues/3899) 193 | 194 | ## 2.0.1 195 | 196 | Fix esbuild property in documentation. 197 | 198 | ## 2.0.0 199 | 200 | Breaking: Use named export instead of default export for better esm/cjs interop. 201 | 202 | To migrate, replace your import by `import { swcReactRefresh } from "vite-plugin-swc-react-refresh";` 203 | 204 | The JSX automatic runtime is also now supported if you bump esbuild to at least [0.14.51](https://github.com/evanw/esbuild/releases/tag/v0.14.51). 205 | 206 | To use it, update your config from `esbuild: { jsxInject: 'import React from "react"' },` to `esbuild: { jsx: "automatic" },` 207 | 208 | ## 0.1.2 209 | 210 | - Add vite as peer dependency 211 | - Pin @swc/core version to 1.2.141 to avoid a 30mb bump of bundle size 212 | 213 | ## 0.1.1 214 | 215 | Add LICENSE 216 | 217 | ## 0.1.0 218 | 219 | Initial release 220 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { dirname, join } from "path"; 3 | import { fileURLToPath } from "url"; 4 | import type { SourceMapPayload } from "module"; 5 | import { 6 | type Output, 7 | type ParserConfig, 8 | type ReactConfig, 9 | type JscTarget, 10 | transform, 11 | type Options as SWCOptions, 12 | } from "@swc/core"; 13 | import type { PluginOption, UserConfig, BuildOptions } from "vite"; 14 | import { createRequire } from "module"; 15 | 16 | const runtimePublicPath = "/@react-refresh"; 17 | 18 | const preambleCode = `import { injectIntoGlobalHook } from "__PATH__"; 19 | injectIntoGlobalHook(window); 20 | window.$RefreshReg$ = () => {}; 21 | window.$RefreshSig$ = () => (type) => type;`; 22 | 23 | const _dirname = 24 | typeof __dirname !== "undefined" 25 | ? __dirname 26 | : dirname(fileURLToPath(import.meta.url)); 27 | const resolve = createRequire( 28 | typeof __filename !== "undefined" ? __filename : import.meta.url, 29 | ).resolve; 30 | const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/; 31 | const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/; 32 | 33 | type Options = { 34 | /** 35 | * Control where the JSX factory is imported from. 36 | * @default "react" 37 | */ 38 | jsxImportSource?: string; 39 | /** 40 | * Enable TypeScript decorators. Requires experimentalDecorators in tsconfig. 41 | * @default false 42 | */ 43 | tsDecorators?: boolean; 44 | /** 45 | * Use SWC plugins. Enable SWC at build time. 46 | * @default undefined 47 | */ 48 | plugins?: [string, Record][]; 49 | /** 50 | * Set the target for SWC in dev. This can avoid to down-transpile private class method for example. 51 | * For production target, see https://vitejs.dev/config/build-options.html#build-target 52 | * @default "es2020" 53 | */ 54 | devTarget?: JscTarget; 55 | /** 56 | * Override the default include list (.ts, .tsx, .mts, .jsx, .mdx). 57 | * This requires to redefine the config for any file you want to be included. 58 | * If you want to trigger fast refresh on compiled JS, use `jsx: true`. 59 | * Exclusion of node_modules should be handled by the function if needed. 60 | */ 61 | parserConfig?: (id: string) => ParserConfig | undefined; 62 | /** 63 | * The future of Vite is with OXC, and from the beginning this was a design choice 64 | * to not exposed too many specialties from SWC so that Vite React users can move to 65 | * another transformer later. 66 | * Also debugging why some specific version of decorators with some other unstable/legacy 67 | * feature doesn't work is not fun, so we won't provide support for it, hence the name `useAtYourOwnRisk` 68 | */ 69 | useAtYourOwnRisk_mutateSwcOptions?: (options: SWCOptions) => void; 70 | }; 71 | 72 | const react = (_options?: Options): PluginOption[] => { 73 | let hmrDisabled = false; 74 | const options = { 75 | jsxImportSource: _options?.jsxImportSource ?? "react", 76 | tsDecorators: _options?.tsDecorators, 77 | plugins: _options?.plugins 78 | ? _options?.plugins.map((el): typeof el => [resolve(el[0]), el[1]]) 79 | : undefined, 80 | devTarget: _options?.devTarget ?? "es2020", 81 | parserConfig: _options?.parserConfig, 82 | useAtYourOwnRisk_mutateSwcOptions: 83 | _options?.useAtYourOwnRisk_mutateSwcOptions, 84 | }; 85 | 86 | return [ 87 | { 88 | name: "vite:react-swc:resolve-runtime", 89 | apply: "serve", 90 | enforce: "pre", // Run before Vite default resolve to avoid syscalls 91 | resolveId: (id) => (id === runtimePublicPath ? id : undefined), 92 | load: (id) => 93 | id === runtimePublicPath 94 | ? readFileSync(join(_dirname, "refresh-runtime.js"), "utf-8") 95 | : undefined, 96 | }, 97 | { 98 | name: "vite:react-swc", 99 | apply: "serve", 100 | config: () => ({ 101 | esbuild: false, 102 | optimizeDeps: { 103 | include: [`${options.jsxImportSource}/jsx-dev-runtime`], 104 | esbuildOptions: { jsx: "automatic" }, 105 | }, 106 | }), 107 | configResolved(config) { 108 | if (config.server.hmr === false) hmrDisabled = true; 109 | const mdxIndex = config.plugins.findIndex( 110 | (p) => p.name === "@mdx-js/rollup", 111 | ); 112 | if ( 113 | mdxIndex !== -1 && 114 | mdxIndex > 115 | config.plugins.findIndex((p) => p.name === "vite:react-swc") 116 | ) { 117 | throw new Error( 118 | "[vite:react-swc] The MDX plugin should be placed before this plugin", 119 | ); 120 | } 121 | }, 122 | transformIndexHtml: (_, config) => [ 123 | { 124 | tag: "script", 125 | attrs: { type: "module" }, 126 | children: preambleCode.replace( 127 | "__PATH__", 128 | config.server!.config.base + runtimePublicPath.slice(1), 129 | ), 130 | }, 131 | ], 132 | async transform(code, _id, transformOptions) { 133 | const id = _id.split("?")[0]; 134 | const refresh = !transformOptions?.ssr && !hmrDisabled; 135 | 136 | const result = await transformWithOptions( 137 | id, 138 | code, 139 | options.devTarget, 140 | options, 141 | { 142 | refresh, 143 | development: true, 144 | runtime: "automatic", 145 | importSource: options.jsxImportSource, 146 | }, 147 | ); 148 | if (!result) return; 149 | if (!refresh) return result; 150 | 151 | const hasRefresh = refreshContentRE.test(result.code); 152 | if (!hasRefresh && !reactCompRE.test(result.code)) return result; 153 | 154 | const sourceMap: SourceMapPayload = JSON.parse(result.map!); 155 | sourceMap.mappings = ";;" + sourceMap.mappings; 156 | 157 | result.code = `import * as RefreshRuntime from "${runtimePublicPath}"; 158 | 159 | ${result.code}`; 160 | 161 | if (hasRefresh) { 162 | sourceMap.mappings = ";;;;;;" + sourceMap.mappings; 163 | result.code = `if (!window.$RefreshReg$) throw new Error("React refresh preamble was not loaded. Something is wrong."); 164 | const prevRefreshReg = window.$RefreshReg$; 165 | const prevRefreshSig = window.$RefreshSig$; 166 | window.$RefreshReg$ = RefreshRuntime.getRefreshReg("${id}"); 167 | window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; 168 | 169 | ${result.code} 170 | 171 | window.$RefreshReg$ = prevRefreshReg; 172 | window.$RefreshSig$ = prevRefreshSig; 173 | `; 174 | } 175 | 176 | result.code += ` 177 | RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => { 178 | RefreshRuntime.registerExportsForReactRefresh("${id}", currentExports); 179 | import.meta.hot.accept((nextExports) => { 180 | if (!nextExports) return; 181 | const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate("${id}", currentExports, nextExports); 182 | if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage); 183 | }); 184 | }); 185 | `; 186 | 187 | return { code: result.code, map: sourceMap }; 188 | }, 189 | }, 190 | options.plugins 191 | ? { 192 | name: "vite:react-swc", 193 | apply: "build", 194 | enforce: "pre", // Run before esbuild 195 | config: (userConfig) => ({ 196 | build: silenceUseClientWarning(userConfig), 197 | }), 198 | transform: (code, _id) => 199 | transformWithOptions(_id.split("?")[0], code, "esnext", options, { 200 | runtime: "automatic", 201 | importSource: options.jsxImportSource, 202 | }), 203 | } 204 | : { 205 | name: "vite:react-swc", 206 | apply: "build", 207 | config: (userConfig) => ({ 208 | build: silenceUseClientWarning(userConfig), 209 | esbuild: { 210 | jsx: "automatic", 211 | jsxImportSource: options.jsxImportSource, 212 | tsconfigRaw: { 213 | compilerOptions: { useDefineForClassFields: true }, 214 | }, 215 | }, 216 | }), 217 | }, 218 | ]; 219 | }; 220 | 221 | const transformWithOptions = async ( 222 | id: string, 223 | code: string, 224 | target: JscTarget, 225 | options: Options, 226 | reactConfig: ReactConfig, 227 | ) => { 228 | const decorators = options?.tsDecorators ?? false; 229 | const parser: ParserConfig | undefined = options.parserConfig 230 | ? options.parserConfig(id) 231 | : id.endsWith(".tsx") 232 | ? { syntax: "typescript", tsx: true, decorators } 233 | : id.endsWith(".ts") || id.endsWith(".mts") 234 | ? { syntax: "typescript", tsx: false, decorators } 235 | : id.endsWith(".jsx") 236 | ? { syntax: "ecmascript", jsx: true } 237 | : id.endsWith(".mdx") 238 | ? // JSX is required to trigger fast refresh transformations, even if MDX already transforms it 239 | { syntax: "ecmascript", jsx: true } 240 | : undefined; 241 | if (!parser) return; 242 | 243 | let result: Output; 244 | try { 245 | const swcOptions: SWCOptions = { 246 | filename: id, 247 | swcrc: false, 248 | configFile: false, 249 | sourceMaps: true, 250 | jsc: { 251 | target, 252 | parser, 253 | experimental: { plugins: options.plugins }, 254 | transform: { 255 | useDefineForClassFields: true, 256 | react: reactConfig, 257 | }, 258 | }, 259 | }; 260 | if (options.useAtYourOwnRisk_mutateSwcOptions) { 261 | options.useAtYourOwnRisk_mutateSwcOptions(swcOptions); 262 | } 263 | result = await transform(code, swcOptions); 264 | } catch (e: any) { 265 | const message: string = e.message; 266 | const fileStartIndex = message.indexOf("╭─["); 267 | if (fileStartIndex !== -1) { 268 | const match = message.slice(fileStartIndex).match(/:(\d+):(\d+)]/); 269 | if (match) { 270 | e.line = match[1]; 271 | e.column = match[2]; 272 | } 273 | } 274 | throw e; 275 | } 276 | 277 | return result; 278 | }; 279 | 280 | const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({ 281 | rollupOptions: { 282 | onwarn(warning, defaultHandler) { 283 | if ( 284 | warning.code === "MODULE_LEVEL_DIRECTIVE" && 285 | warning.message.includes("use client") 286 | ) { 287 | return; 288 | } 289 | // https://github.com/vitejs/vite/issues/15012 290 | if ( 291 | warning.code === "SOURCEMAP_ERROR" && 292 | warning.message.includes("resolve original location") && 293 | warning.pos === 0 294 | ) { 295 | return; 296 | } 297 | if (userConfig.build?.rollupOptions?.onwarn) { 298 | userConfig.build.rollupOptions.onwarn(warning, defaultHandler); 299 | } else { 300 | defaultHandler(warning); 301 | } 302 | }, 303 | }, 304 | }); 305 | 306 | export default react; 307 | -------------------------------------------------------------------------------- /src/refresh-runtime.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) Meta Platforms, Inc. and affiliates. **/ 2 | /** 3 | * This is simplified pure-js version of https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshRuntime.js 4 | * without IE11 compatibility and verbose isDev checks. 5 | * Some utils are appended at the bottom for HMR integration. 6 | */ 7 | 8 | const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); 9 | const REACT_MEMO_TYPE = Symbol.for("react.memo"); 10 | 11 | // We never remove these associations. 12 | // It's OK to reference families, but use WeakMap/Set for types. 13 | let allFamiliesByID = new Map(); 14 | let allFamiliesByType = new WeakMap(); 15 | let allSignaturesByType = new WeakMap(); 16 | 17 | // This WeakMap is read by React, so we only put families 18 | // that have actually been edited here. This keeps checks fast. 19 | const updatedFamiliesByType = new WeakMap(); 20 | 21 | // This is cleared on every performReactRefresh() call. 22 | // It is an array of [Family, NextType] tuples. 23 | let pendingUpdates = []; 24 | 25 | // This is injected by the renderer via DevTools global hook. 26 | const helpersByRendererID = new Map(); 27 | 28 | const helpersByRoot = new Map(); 29 | 30 | // We keep track of mounted roots so we can schedule updates. 31 | const mountedRoots = new Set(); 32 | // If a root captures an error, we remember it so we can retry on edit. 33 | const failedRoots = new Set(); 34 | 35 | // We also remember the last element for every root. 36 | // It needs to be weak because we do this even for roots that failed to mount. 37 | // If there is no WeakMap, we won't attempt to do retrying. 38 | let rootElements = new WeakMap(); 39 | let isPerformingRefresh = false; 40 | 41 | function computeFullKey(signature) { 42 | if (signature.fullKey !== null) { 43 | return signature.fullKey; 44 | } 45 | 46 | let fullKey = signature.ownKey; 47 | let hooks; 48 | try { 49 | hooks = signature.getCustomHooks(); 50 | } catch (err) { 51 | // This can happen in an edge case, e.g. if expression like Foo.useSomething 52 | // depends on Foo which is lazily initialized during rendering. 53 | // In that case just assume we'll have to remount. 54 | signature.forceReset = true; 55 | signature.fullKey = fullKey; 56 | return fullKey; 57 | } 58 | 59 | for (let i = 0; i < hooks.length; i++) { 60 | const hook = hooks[i]; 61 | if (typeof hook !== "function") { 62 | // Something's wrong. Assume we need to remount. 63 | signature.forceReset = true; 64 | signature.fullKey = fullKey; 65 | return fullKey; 66 | } 67 | const nestedHookSignature = allSignaturesByType.get(hook); 68 | if (nestedHookSignature === undefined) { 69 | // No signature means Hook wasn't in the source code, e.g. in a library. 70 | // We'll skip it because we can assume it won't change during this session. 71 | continue; 72 | } 73 | const nestedHookKey = computeFullKey(nestedHookSignature); 74 | if (nestedHookSignature.forceReset) { 75 | signature.forceReset = true; 76 | } 77 | fullKey += "\n---\n" + nestedHookKey; 78 | } 79 | 80 | signature.fullKey = fullKey; 81 | return fullKey; 82 | } 83 | 84 | function haveEqualSignatures(prevType, nextType) { 85 | const prevSignature = allSignaturesByType.get(prevType); 86 | const nextSignature = allSignaturesByType.get(nextType); 87 | 88 | if (prevSignature === undefined && nextSignature === undefined) { 89 | return true; 90 | } 91 | if (prevSignature === undefined || nextSignature === undefined) { 92 | return false; 93 | } 94 | if (computeFullKey(prevSignature) !== computeFullKey(nextSignature)) { 95 | return false; 96 | } 97 | if (nextSignature.forceReset) { 98 | return false; 99 | } 100 | 101 | return true; 102 | } 103 | 104 | function isReactClass(type) { 105 | return type.prototype && type.prototype.isReactComponent; 106 | } 107 | 108 | function canPreserveStateBetween(prevType, nextType) { 109 | if (isReactClass(prevType) || isReactClass(nextType)) { 110 | return false; 111 | } 112 | if (haveEqualSignatures(prevType, nextType)) { 113 | return true; 114 | } 115 | return false; 116 | } 117 | 118 | function resolveFamily(type) { 119 | // Only check updated types to keep lookups fast. 120 | return updatedFamiliesByType.get(type); 121 | } 122 | 123 | // This is a safety mechanism to protect against rogue getters and Proxies. 124 | function getProperty(object, property) { 125 | try { 126 | return object[property]; 127 | } catch (err) { 128 | // Intentionally ignore. 129 | return undefined; 130 | } 131 | } 132 | 133 | function performReactRefresh() { 134 | if (pendingUpdates.length === 0) { 135 | return null; 136 | } 137 | if (isPerformingRefresh) { 138 | return null; 139 | } 140 | 141 | isPerformingRefresh = true; 142 | try { 143 | const staleFamilies = new Set(); 144 | const updatedFamilies = new Set(); 145 | 146 | const updates = pendingUpdates; 147 | pendingUpdates = []; 148 | updates.forEach(([family, nextType]) => { 149 | // Now that we got a real edit, we can create associations 150 | // that will be read by the React reconciler. 151 | const prevType = family.current; 152 | updatedFamiliesByType.set(prevType, family); 153 | updatedFamiliesByType.set(nextType, family); 154 | family.current = nextType; 155 | 156 | // Determine whether this should be a re-render or a re-mount. 157 | if (canPreserveStateBetween(prevType, nextType)) { 158 | updatedFamilies.add(family); 159 | } else { 160 | staleFamilies.add(family); 161 | } 162 | }); 163 | 164 | // TODO: rename these fields to something more meaningful. 165 | const update = { 166 | updatedFamilies, // Families that will re-render preserving state 167 | staleFamilies, // Families that will be remounted 168 | }; 169 | 170 | helpersByRendererID.forEach((helpers) => { 171 | // Even if there are no roots, set the handler on first update. 172 | // This ensures that if *new* roots are mounted, they'll use the resolve handler. 173 | helpers.setRefreshHandler(resolveFamily); 174 | }); 175 | 176 | let didError = false; 177 | let firstError = null; 178 | 179 | // We snapshot maps and sets that are mutated during commits. 180 | // If we don't do this, there is a risk they will be mutated while 181 | // we iterate over them. For example, trying to recover a failed root 182 | // may cause another root to be added to the failed list -- an infinite loop. 183 | const failedRootsSnapshot = new Set(failedRoots); 184 | const mountedRootsSnapshot = new Set(mountedRoots); 185 | const helpersByRootSnapshot = new Map(helpersByRoot); 186 | 187 | failedRootsSnapshot.forEach((root) => { 188 | const helpers = helpersByRootSnapshot.get(root); 189 | if (helpers === undefined) { 190 | throw new Error( 191 | "Could not find helpers for a root. This is a bug in React Refresh.", 192 | ); 193 | } 194 | if (!failedRoots.has(root)) { 195 | // No longer failed. 196 | } 197 | if (rootElements === null) { 198 | return; 199 | } 200 | if (!rootElements.has(root)) { 201 | return; 202 | } 203 | const element = rootElements.get(root); 204 | try { 205 | helpers.scheduleRoot(root, element); 206 | } catch (err) { 207 | if (!didError) { 208 | didError = true; 209 | firstError = err; 210 | } 211 | // Keep trying other roots. 212 | } 213 | }); 214 | mountedRootsSnapshot.forEach((root) => { 215 | const helpers = helpersByRootSnapshot.get(root); 216 | if (helpers === undefined) { 217 | throw new Error( 218 | "Could not find helpers for a root. This is a bug in React Refresh.", 219 | ); 220 | } 221 | if (!mountedRoots.has(root)) { 222 | // No longer mounted. 223 | } 224 | try { 225 | helpers.scheduleRefresh(root, update); 226 | } catch (err) { 227 | if (!didError) { 228 | didError = true; 229 | firstError = err; 230 | } 231 | // Keep trying other roots. 232 | } 233 | }); 234 | if (didError) { 235 | throw firstError; 236 | } 237 | return update; 238 | } finally { 239 | isPerformingRefresh = false; 240 | } 241 | } 242 | 243 | function register(type, id) { 244 | if (type === null) { 245 | return; 246 | } 247 | if (typeof type !== "function" && typeof type !== "object") { 248 | return; 249 | } 250 | 251 | // This can happen in an edge case, e.g. if we register 252 | // return value of a HOC but it returns a cached component. 253 | // Ignore anything but the first registration for each type. 254 | if (allFamiliesByType.has(type)) { 255 | return; 256 | } 257 | // Create family or remember to update it. 258 | // None of this bookkeeping affects reconciliation 259 | // until the first performReactRefresh() call above. 260 | let family = allFamiliesByID.get(id); 261 | if (family === undefined) { 262 | family = { current: type }; 263 | allFamiliesByID.set(id, family); 264 | } else { 265 | pendingUpdates.push([family, type]); 266 | } 267 | allFamiliesByType.set(type, family); 268 | 269 | // Visit inner types because we might not have registered them. 270 | if (typeof type === "object" && type !== null) { 271 | switch (getProperty(type, "$$typeof")) { 272 | case REACT_FORWARD_REF_TYPE: 273 | register(type.render, id + "$render"); 274 | break; 275 | case REACT_MEMO_TYPE: 276 | register(type.type, id + "$type"); 277 | break; 278 | } 279 | } 280 | } 281 | 282 | function setSignature(type, key, forceReset, getCustomHooks) { 283 | if (!allSignaturesByType.has(type)) { 284 | allSignaturesByType.set(type, { 285 | forceReset, 286 | ownKey: key, 287 | fullKey: null, 288 | getCustomHooks: getCustomHooks || (() => []), 289 | }); 290 | } 291 | // Visit inner types because we might not have signed them. 292 | if (typeof type === "object" && type !== null) { 293 | switch (getProperty(type, "$$typeof")) { 294 | case REACT_FORWARD_REF_TYPE: 295 | setSignature(type.render, key, forceReset, getCustomHooks); 296 | break; 297 | case REACT_MEMO_TYPE: 298 | setSignature(type.type, key, forceReset, getCustomHooks); 299 | break; 300 | } 301 | } 302 | } 303 | 304 | // This is lazily called during first render for a type. 305 | // It captures Hook list at that time so inline requires don't break comparisons. 306 | function collectCustomHooksForSignature(type) { 307 | const signature = allSignaturesByType.get(type); 308 | if (signature !== undefined) { 309 | computeFullKey(signature); 310 | } 311 | } 312 | 313 | export function injectIntoGlobalHook(globalObject) { 314 | // For React Native, the global hook will be set up by require('react-devtools-core'). 315 | // That code will run before us. So we need to monkeypatch functions on existing hook. 316 | 317 | // For React Web, the global hook will be set up by the extension. 318 | // This will also run before us. 319 | let hook = globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__; 320 | if (hook === undefined) { 321 | // However, if there is no DevTools extension, we'll need to set up the global hook ourselves. 322 | // Note that in this case it's important that renderer code runs *after* this method call. 323 | // Otherwise, the renderer will think that there is no global hook, and won't do the injection. 324 | let nextID = 0; 325 | globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = { 326 | renderers: new Map(), 327 | supportsFiber: true, 328 | inject: (injected) => nextID++, 329 | onScheduleFiberRoot: (id, root, children) => {}, 330 | onCommitFiberRoot: (id, root, maybePriorityLevel, didError) => {}, 331 | onCommitFiberUnmount() {}, 332 | }; 333 | } 334 | 335 | if (hook.isDisabled) { 336 | // This isn't a real property on the hook, but it can be set to opt out 337 | // of DevTools integration and associated warnings and logs. 338 | // Using console['warn'] to evade Babel and ESLint 339 | console["warn"]( 340 | "Something has shimmed the React DevTools global hook (__REACT_DEVTOOLS_GLOBAL_HOOK__). " + 341 | "Fast Refresh is not compatible with this shim and will be disabled.", 342 | ); 343 | return; 344 | } 345 | 346 | // Here, we just want to get a reference to scheduleRefresh. 347 | const oldInject = hook.inject; 348 | hook.inject = function (injected) { 349 | const id = oldInject.apply(this, arguments); 350 | if ( 351 | typeof injected.scheduleRefresh === "function" && 352 | typeof injected.setRefreshHandler === "function" 353 | ) { 354 | // This version supports React Refresh. 355 | helpersByRendererID.set(id, injected); 356 | } 357 | return id; 358 | }; 359 | 360 | // Do the same for any already injected roots. 361 | // This is useful if ReactDOM has already been initialized. 362 | // https://github.com/facebook/react/issues/17626 363 | hook.renderers.forEach((injected, id) => { 364 | if ( 365 | typeof injected.scheduleRefresh === "function" && 366 | typeof injected.setRefreshHandler === "function" 367 | ) { 368 | // This version supports React Refresh. 369 | helpersByRendererID.set(id, injected); 370 | } 371 | }); 372 | 373 | // We also want to track currently mounted roots. 374 | const oldOnCommitFiberRoot = hook.onCommitFiberRoot; 375 | const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {}); 376 | hook.onScheduleFiberRoot = function (id, root, children) { 377 | if (!isPerformingRefresh) { 378 | // If it was intentionally scheduled, don't attempt to restore. 379 | // This includes intentionally scheduled unmounts. 380 | failedRoots.delete(root); 381 | if (rootElements !== null) { 382 | rootElements.set(root, children); 383 | } 384 | } 385 | return oldOnScheduleFiberRoot.apply(this, arguments); 386 | }; 387 | hook.onCommitFiberRoot = function (id, root, maybePriorityLevel, didError) { 388 | const helpers = helpersByRendererID.get(id); 389 | if (helpers !== undefined) { 390 | helpersByRoot.set(root, helpers); 391 | 392 | const current = root.current; 393 | const alternate = current.alternate; 394 | 395 | // We need to determine whether this root has just (un)mounted. 396 | // This logic is copy-pasted from similar logic in the DevTools backend. 397 | // If this breaks with some refactoring, you'll want to update DevTools too. 398 | 399 | if (alternate !== null) { 400 | const wasMounted = 401 | alternate.memoizedState != null && 402 | alternate.memoizedState.element != null && 403 | mountedRoots.has(root); 404 | 405 | const isMounted = 406 | current.memoizedState != null && 407 | current.memoizedState.element != null; 408 | 409 | if (!wasMounted && isMounted) { 410 | // Mount a new root. 411 | mountedRoots.add(root); 412 | failedRoots.delete(root); 413 | } else if (wasMounted && isMounted) { 414 | // Update an existing root. 415 | // This doesn't affect our mounted root Set. 416 | } else if (wasMounted && !isMounted) { 417 | // Unmount an existing root. 418 | mountedRoots.delete(root); 419 | if (didError) { 420 | // We'll remount it on future edits. 421 | failedRoots.add(root); 422 | } else { 423 | helpersByRoot.delete(root); 424 | } 425 | } else if (!wasMounted && !isMounted) { 426 | if (didError) { 427 | // We'll remount it on future edits. 428 | failedRoots.add(root); 429 | } 430 | } 431 | } else { 432 | // Mount a new root. 433 | mountedRoots.add(root); 434 | } 435 | } 436 | 437 | // Always call the decorated DevTools hook. 438 | return oldOnCommitFiberRoot.apply(this, arguments); 439 | }; 440 | } 441 | 442 | // This is a wrapper over more primitive functions for setting signature. 443 | // Signatures let us decide whether the Hook order has changed on refresh. 444 | // 445 | // This function is intended to be used as a transform target, e.g.: 446 | // var _s = createSignatureFunctionForTransform() 447 | // 448 | // function Hello() { 449 | // const [foo, setFoo] = useState(0); 450 | // const value = useCustomHook(); 451 | // _s(); /* Call without arguments triggers collecting the custom Hook list. 452 | // * This doesn't happen during the module evaluation because we 453 | // * don't want to change the module order with inline requires. 454 | // * Next calls are noops. */ 455 | // return

Hi

; 456 | // } 457 | // 458 | // /* Call with arguments attaches the signature to the type: */ 459 | // _s( 460 | // Hello, 461 | // 'useState{[foo, setFoo]}(0)', 462 | // () => [useCustomHook], /* Lazy to avoid triggering inline requires */ 463 | // ); 464 | export function createSignatureFunctionForTransform() { 465 | let savedType; 466 | let hasCustomHooks; 467 | let didCollectHooks = false; 468 | return function (type, key, forceReset, getCustomHooks) { 469 | if (typeof key === "string") { 470 | // We're in the initial phase that associates signatures 471 | // with the functions. Note this may be called multiple times 472 | // in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))). 473 | if (!savedType) { 474 | // We're in the innermost call, so this is the actual type. 475 | // $FlowFixMe[escaped-generic] discovered when updating Flow 476 | savedType = type; 477 | hasCustomHooks = typeof getCustomHooks === "function"; 478 | } 479 | // Set the signature for all types (even wrappers!) in case 480 | // they have no signatures of their own. This is to prevent 481 | // problems like https://github.com/facebook/react/issues/20417. 482 | if ( 483 | type != null && 484 | (typeof type === "function" || typeof type === "object") 485 | ) { 486 | setSignature(type, key, forceReset, getCustomHooks); 487 | } 488 | return type; 489 | } else { 490 | // We're in the _s() call without arguments, which means 491 | // this is the time to collect custom Hook signatures. 492 | // Only do this once. This path is hot and runs *inside* every render! 493 | if (!didCollectHooks && hasCustomHooks) { 494 | didCollectHooks = true; 495 | collectCustomHooksForSignature(savedType); 496 | } 497 | } 498 | }; 499 | } 500 | 501 | function isLikelyComponentType(type) { 502 | switch (typeof type) { 503 | case "function": { 504 | // First, deal with classes. 505 | if (type.prototype != null) { 506 | if (type.prototype.isReactComponent) { 507 | // React class. 508 | return true; 509 | } 510 | const ownNames = Object.getOwnPropertyNames(type.prototype); 511 | if (ownNames.length > 1 || ownNames[0] !== "constructor") { 512 | // This looks like a class. 513 | return false; 514 | } 515 | // eslint-disable-next-line no-proto 516 | if (type.prototype.__proto__ !== Object.prototype) { 517 | // It has a superclass. 518 | return false; 519 | } 520 | // Pass through. 521 | // This looks like a regular function with empty prototype. 522 | } 523 | // For plain functions and arrows, use name as a heuristic. 524 | const name = type.name || type.displayName; 525 | return typeof name === "string" && /^[A-Z]/.test(name); 526 | } 527 | case "object": { 528 | if (type != null) { 529 | switch (getProperty(type, "$$typeof")) { 530 | case REACT_FORWARD_REF_TYPE: 531 | case REACT_MEMO_TYPE: 532 | // Definitely React components. 533 | return true; 534 | default: 535 | return false; 536 | } 537 | } 538 | return false; 539 | } 540 | default: { 541 | return false; 542 | } 543 | } 544 | } 545 | 546 | /** 547 | * Plugin utils 548 | */ 549 | 550 | export function getRefreshReg(filename) { 551 | return (type, id) => register(type, filename + " " + id); 552 | } 553 | 554 | // Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141 555 | // This allows to resister components not detected by SWC like styled component 556 | export function registerExportsForReactRefresh(filename, moduleExports) { 557 | for (const key in moduleExports) { 558 | if (key === "__esModule") continue; 559 | const exportValue = moduleExports[key]; 560 | if (isLikelyComponentType(exportValue)) { 561 | // 'export' is required to avoid key collision when renamed exports that 562 | // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116 563 | // The register function has an identity check to not register twice the same component, 564 | // so this is safe to not used the same key here. 565 | register(exportValue, filename + " export " + key); 566 | } 567 | } 568 | } 569 | 570 | function debounce(fn, delay) { 571 | let handle; 572 | return () => { 573 | clearTimeout(handle); 574 | handle = setTimeout(fn, delay); 575 | }; 576 | } 577 | 578 | const hooks = []; 579 | window.__registerBeforePerformReactRefresh = (cb) => { 580 | hooks.push(cb); 581 | }; 582 | const enqueueUpdate = debounce(async () => { 583 | if (hooks.length) await Promise.all(hooks.map((cb) => cb())); 584 | performReactRefresh(); 585 | }, 16); 586 | 587 | export function validateRefreshBoundaryAndEnqueueUpdate( 588 | id, 589 | prevExports, 590 | nextExports, 591 | ) { 592 | const ignoredExports = window.__getReactRefreshIgnoredExports?.({ id }) ?? []; 593 | if ( 594 | predicateOnExport( 595 | ignoredExports, 596 | prevExports, 597 | (key) => key in nextExports, 598 | ) !== true 599 | ) { 600 | return "Could not Fast Refresh (export removed)"; 601 | } 602 | if ( 603 | predicateOnExport( 604 | ignoredExports, 605 | nextExports, 606 | (key) => key in prevExports, 607 | ) !== true 608 | ) { 609 | return "Could not Fast Refresh (new export)"; 610 | } 611 | 612 | let hasExports = false; 613 | const allExportsAreComponentsOrUnchanged = predicateOnExport( 614 | ignoredExports, 615 | nextExports, 616 | (key, value) => { 617 | hasExports = true; 618 | if (isLikelyComponentType(value)) return true; 619 | return prevExports[key] === nextExports[key]; 620 | }, 621 | ); 622 | if (hasExports && allExportsAreComponentsOrUnchanged === true) { 623 | enqueueUpdate(); 624 | } else { 625 | return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react-swc#consistent-components-exports`; 626 | } 627 | } 628 | 629 | function predicateOnExport(ignoredExports, moduleExports, predicate) { 630 | for (const key in moduleExports) { 631 | if (key === "__esModule") continue; 632 | if (ignoredExports.includes(key)) continue; 633 | const desc = Object.getOwnPropertyDescriptor(moduleExports, key); 634 | if (desc && desc.get) return key; 635 | if (!predicate(key, moduleExports[key])) return key; 636 | } 637 | return true; 638 | } 639 | 640 | // Hides vite-ignored dynamic import so that Vite can skip analysis if no other 641 | // dynamic import is present (https://github.com/vitejs/vite/pull/12732) 642 | export const __hmr_import = (module) => import(/* @vite-ignore */ module); 643 | 644 | // For backwards compatibility with @vitejs/plugin-react. 645 | export default { injectIntoGlobalHook }; 646 | --------------------------------------------------------------------------------