# Vite Plugin React
-See [`@vitejs/plugin-react` documentation](packages/plugin-react/README.md)
+See [`@vitejs/plugin-react` documentation](packages/plugin-react/README.md) and [`@vitejs/plugin-react-swc` documentation](packages/plugin-react-swc/README.md)
+
+# Vite Plugin RSC
+
+See [`@vitejs/plugin-rsc` documentation](packages/plugin-rsc/README.md)
## Packages
-| Package | Version (click for changelogs) |
-| --------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
-| [@vitejs/plugin-react](packages/plugin-react) | [](packages/plugin-react/CHANGELOG.md) |
+| Package | Version (click for changelogs) |
+| ----------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
+| [@vitejs/plugin-react](packages/plugin-react) | [](packages/plugin-react/CHANGELOG.md) |
+| [@vitejs/plugin-react-swc](packages/plugin-react-swc) | [](packages/plugin-react-swc/CHANGELOG.md) |
+| [@vitejs/plugin-rsc](packages/plugin-rsc) | [](packages/plugin-rsc/CHANGELOG.md) |
+| [@vitejs/plugin-react-oxc](packages/plugin-react-oxc) | [Deprecated](packages/plugin-react-oxc/CHANGELOG.md), merged with [`@vitejs/plugin-react`](packages/plugin-react) |
## License
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 000000000..1b2ff21dc
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,171 @@
+// @ts-check
+import { builtinModules } from 'node:module'
+import eslint from '@eslint/js'
+import pluginN from 'eslint-plugin-n'
+import pluginImportX from 'eslint-plugin-import-x'
+import pluginRegExp from 'eslint-plugin-regexp'
+import tseslint from 'typescript-eslint'
+import globals from 'globals'
+
+export default tseslint.config(
+ {
+ ignores: [
+ '**/dist/**',
+ '**/playground-temp/**',
+ '**/temp/**',
+ 'packages/plugin-rsc/**',
+ ],
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...tseslint.configs.stylistic,
+ pluginN.configs['flat/recommended'],
+ pluginRegExp.configs['flat/recommended'],
+ {
+ name: 'main',
+ languageOptions: {
+ parser: tseslint.parser,
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2021,
+ },
+ globals: {
+ ...globals.es2021,
+ ...globals.node,
+ },
+ },
+ plugins: {
+ n: pluginN,
+ 'import-x': pluginImportX,
+ },
+ rules: {
+ eqeqeq: ['warn', 'always', { null: 'never' }],
+ 'no-debugger': ['error'],
+ 'no-empty': ['warn', { allowEmptyCatch: true }],
+ 'prefer-const': [
+ 'warn',
+ {
+ destructuring: 'all',
+ },
+ ],
+
+ 'n/no-process-exit': 'off',
+ 'n/no-deprecated-api': 'off',
+ 'n/no-unpublished-import': 'off',
+ 'n/no-unpublished-require': 'off',
+ 'n/no-unsupported-features/es-syntax': 'off',
+ 'n/no-missing-import': [
+ 'error',
+ {
+ tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'],
+ },
+ ],
+
+ '@typescript-eslint/explicit-module-boundary-types': [
+ 'error',
+ { allowArgumentsExplicitlyTypedAsAny: true },
+ ],
+ '@typescript-eslint/no-empty-function': [
+ 'error',
+ { allow: ['arrowFunctions'] },
+ ],
+ '@typescript-eslint/no-empty-interface': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ 'no-extra-semi': 'off',
+ '@typescript-eslint/no-extra-semi': 'off', // conflicts with prettier
+ '@typescript-eslint/no-inferrable-types': 'off',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ args: 'all',
+ argsIgnorePattern: '^_',
+ caughtErrors: 'all',
+ caughtErrorsIgnorePattern: '^_',
+ destructuredArrayIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ ignoreRestSiblings: true,
+ },
+ ],
+ '@typescript-eslint/no-var-requires': 'off',
+ '@typescript-eslint/consistent-type-imports': [
+ 'error',
+ { prefer: 'type-imports', disallowTypeAnnotations: false },
+ ],
+ // disable rules set in @typescript-eslint/stylistic which conflict with current code
+ // we should discuss if we want to enable these as they encourage consistent code
+ '@typescript-eslint/array-type': 'off',
+ '@typescript-eslint/consistent-type-definitions': 'off',
+ '@typescript-eslint/prefer-for-of': 'off',
+ '@typescript-eslint/prefer-function-type': 'off',
+
+ 'import-x/no-nodejs-modules': [
+ 'error',
+ { allow: builtinModules.map((mod) => `node:${mod}`) },
+ ],
+ 'import-x/no-duplicates': 'error',
+ 'import-x/order': 'error',
+ 'sort-imports': [
+ 'error',
+ {
+ ignoreCase: false,
+ ignoreDeclarationSort: true,
+ ignoreMemberSort: false,
+ memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
+ allowSeparatedGroups: false,
+ },
+ ],
+
+ 'regexp/prefer-regexp-exec': 'error',
+ 'regexp/prefer-regexp-test': 'error',
+ // in some cases using explicit letter-casing is more performant than the `i` flag
+ 'regexp/use-ignore-case': 'off',
+ },
+ },
+ {
+ name: 'vite/globals',
+ files: ['packages/**/*.?([cm])[jt]s?(x)'],
+ ignores: ['**/__tests__/**'],
+ rules: {
+ 'no-restricted-globals': ['error', 'require', '__dirname', '__filename'],
+ },
+ },
+ {
+ name: 'disables/playground',
+ files: [
+ 'packages/**/*.test.?([cm])[jt]s?(x)',
+ 'playground/**/*.?([cm])[jt]s?(x)',
+ 'packages/plugin-react-swc/playground/**/*.?([cm])[jt]s?(x)',
+ ],
+ rules: {
+ 'n/no-extraneous-import': 'off',
+ 'n/no-extraneous-require': 'off',
+ 'n/no-missing-import': 'off',
+ 'n/no-missing-require': 'off',
+ 'n/no-unsupported-features/es-builtins': 'off',
+ 'n/no-unsupported-features/node-builtins': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-unused-expressions': 'off',
+ '@typescript-eslint/no-unused-vars': 'off',
+ 'no-undef': 'off',
+ 'no-empty': 'off',
+ 'no-constant-condition': 'off',
+ '@typescript-eslint/no-empty-function': 'off',
+ },
+ },
+ {
+ name: 'disables/js',
+ files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
+ rules: {
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-unused-vars': 'off',
+ },
+ },
+ {
+ name: 'disables/dts',
+ files: ['**/*.d.ts'],
+ rules: {
+ '@typescript-eslint/consistent-indexed-object-style': 'off',
+ '@typescript-eslint/triple-slash-reference': 'off',
+ },
+ },
+)
diff --git a/package.json b/package.json
index 53ebec5ba..637fcf6e0 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,9 @@
"private": true,
"type": "module",
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
+ "packageManager": "pnpm@10.18.0",
"homepage": "https://github.com/vitejs/vite-plugin-react/",
"keywords": [
"frontend",
@@ -20,40 +21,38 @@
"format": "prettier --write --cache .",
"lint": "eslint --cache .",
"typecheck": "tsc -p scripts && tsc -p playground && tsc -p packages/plugin-react",
- "test": "run-s test-serve test-build",
+ "test": "pnpm run test-unit && pnpm run test-serve && pnpm run test-build && pnpm --filter ./packages/plugin-react-swc run test",
+ "test-unit": "pnpm -r --filter='./packages/*' run test-unit",
"test-serve": "vitest run -c playground/vitest.config.e2e.ts",
"test-build": "VITE_TEST_BUILD=1 vitest run -c playground/vitest.config.e2e.ts",
"debug-serve": "VITE_DEBUG_SERVE=1 vitest run -c playground/vitest.config.e2e.ts",
"debug-build": "VITE_TEST_BUILD=1 VITE_PRESERVE_BUILD_ARTIFACTS=1 vitest run -c playground/vitest.config.e2e.ts",
"build": "pnpm -r --filter='./packages/*' run build",
"dev": "pnpm -r --parallel --filter='./packages/*' run dev",
- "release": "tsx scripts/release.ts",
- "ci-publish": "tsx scripts/publishCI.ts"
+ "release": "node scripts/release.ts",
+ "ci-publish": "node scripts/publishCI.ts"
},
"devDependencies": {
- "@eslint-types/import": "^2.29.1",
- "@eslint-types/typescript-eslint": "^7.5.0",
+ "@eslint/js": "^9.37.0",
"@types/fs-extra": "^11.0.4",
- "@types/node": "^20.12.12",
- "@typescript-eslint/eslint-plugin": "^7.9.0",
- "@typescript-eslint/parser": "^7.9.0",
- "@vitejs/release-scripts": "^1.3.1",
- "eslint": "^8.57.0",
- "eslint-define-config": "^2.1.0",
- "eslint-plugin-import": "^2.29.1",
- "eslint-plugin-n": "^17.7.0",
- "eslint-plugin-regexp": "^2.5.0",
- "fs-extra": "^11.2.0",
- "lint-staged": "^15.2.2",
- "npm-run-all2": "^6.2.0",
- "picocolors": "^1.0.1",
- "playwright-chromium": "^1.44.0",
- "prettier": "^3.0.3",
- "simple-git-hooks": "^2.11.1",
- "tsx": "^4.10.5",
- "typescript": "^5.4.5",
- "vite": "^5.2.11",
- "vitest": "^1.6.0"
+ "@types/node": "^22.18.8",
+ "@vitejs/release-scripts": "^1.6.0",
+ "eslint": "^9.37.0",
+ "eslint-plugin-import-x": "^4.16.1",
+ "eslint-plugin-n": "^17.23.1",
+ "eslint-plugin-regexp": "^2.10.0",
+ "fs-extra": "^11.3.2",
+ "globals": "^16.4.0",
+ "lint-staged": "^16.2.3",
+ "picocolors": "^1.1.1",
+ "playwright-chromium": "^1.55.1",
+ "prettier": "^3.6.2",
+ "simple-git-hooks": "^2.13.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.45.0",
+ "vite": "^7.1.9",
+ "vite-plugin-inspect": "^11.3.3",
+ "vitest": "^3.2.4"
},
"simple-git-hooks": {
"pre-commit": "pnpm exec lint-staged --concurrent false"
@@ -71,6 +70,5 @@
"playground/**/__tests__/**/*.ts": [
"eslint --cache --fix"
]
- },
- "packageManager": "pnpm@8.11.0"
+ }
}
diff --git a/packages/common/index.ts b/packages/common/index.ts
new file mode 100644
index 000000000..e194bed42
--- /dev/null
+++ b/packages/common/index.ts
@@ -0,0 +1,2 @@
+export * from './refresh-utils'
+export * from './warning'
diff --git a/packages/common/package.json b/packages/common/package.json
new file mode 100644
index 000000000..1bb806f8c
--- /dev/null
+++ b/packages/common/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@vitejs/react-common",
+ "version": "0.0.0",
+ "type": "module",
+ "private": true,
+ "exports": {
+ ".": "./index.ts",
+ "./refresh-runtime": "./refresh-runtime.js"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ }
+}
diff --git a/packages/common/refresh-runtime.js b/packages/common/refresh-runtime.js
new file mode 100644
index 000000000..e798d000e
--- /dev/null
+++ b/packages/common/refresh-runtime.js
@@ -0,0 +1,663 @@
+/* global window */
+/* eslint-disable eqeqeq, prefer-const, @typescript-eslint/no-empty-function */
+
+/*! Copyright (c) Meta Platforms, Inc. and affiliates. **/
+/**
+ * This is simplified pure-js version of https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshRuntime.js
+ * without IE11 compatibility and verbose isDev checks.
+ * Some utils are appended at the bottom for HMR integration.
+ */
+
+const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref')
+const REACT_MEMO_TYPE = Symbol.for('react.memo')
+
+// We never remove these associations.
+// It's OK to reference families, but use WeakMap/Set for types.
+let allFamiliesByID = new Map()
+let allFamiliesByType = new WeakMap()
+let allSignaturesByType = new WeakMap()
+
+// This WeakMap is read by React, so we only put families
+// that have actually been edited here. This keeps checks fast.
+const updatedFamiliesByType = new WeakMap()
+
+// This is cleared on every performReactRefresh() call.
+// It is an array of [Family, NextType] tuples.
+let pendingUpdates = []
+
+// This is injected by the renderer via DevTools global hook.
+const helpersByRendererID = new Map()
+
+const helpersByRoot = new Map()
+
+// We keep track of mounted roots so we can schedule updates.
+const mountedRoots = new Set()
+// If a root captures an error, we remember it so we can retry on edit.
+const failedRoots = new Set()
+
+// We also remember the last element for every root.
+// It needs to be weak because we do this even for roots that failed to mount.
+// If there is no WeakMap, we won't attempt to do retrying.
+let rootElements = new WeakMap()
+let isPerformingRefresh = false
+
+function computeFullKey(signature) {
+ if (signature.fullKey !== null) {
+ return signature.fullKey
+ }
+
+ let fullKey = signature.ownKey
+ let hooks
+ try {
+ hooks = signature.getCustomHooks()
+ } catch (err) {
+ // This can happen in an edge case, e.g. if expression like Foo.useSomething
+ // depends on Foo which is lazily initialized during rendering.
+ // In that case just assume we'll have to remount.
+ signature.forceReset = true
+ signature.fullKey = fullKey
+ return fullKey
+ }
+
+ for (let i = 0; i < hooks.length; i++) {
+ const hook = hooks[i]
+ if (typeof hook !== 'function') {
+ // Something's wrong. Assume we need to remount.
+ signature.forceReset = true
+ signature.fullKey = fullKey
+ return fullKey
+ }
+ const nestedHookSignature = allSignaturesByType.get(hook)
+ if (nestedHookSignature === undefined) {
+ // No signature means Hook wasn't in the source code, e.g. in a library.
+ // We'll skip it because we can assume it won't change during this session.
+ continue
+ }
+ const nestedHookKey = computeFullKey(nestedHookSignature)
+ if (nestedHookSignature.forceReset) {
+ signature.forceReset = true
+ }
+ fullKey += '\n---\n' + nestedHookKey
+ }
+
+ signature.fullKey = fullKey
+ return fullKey
+}
+
+function haveEqualSignatures(prevType, nextType) {
+ const prevSignature = allSignaturesByType.get(prevType)
+ const nextSignature = allSignaturesByType.get(nextType)
+
+ if (prevSignature === undefined && nextSignature === undefined) {
+ return true
+ }
+ if (prevSignature === undefined || nextSignature === undefined) {
+ return false
+ }
+ if (computeFullKey(prevSignature) !== computeFullKey(nextSignature)) {
+ return false
+ }
+ if (nextSignature.forceReset) {
+ return false
+ }
+
+ return true
+}
+
+function isReactClass(type) {
+ return type.prototype && type.prototype.isReactComponent
+}
+
+function canPreserveStateBetween(prevType, nextType) {
+ if (isReactClass(prevType) || isReactClass(nextType)) {
+ return false
+ }
+ if (haveEqualSignatures(prevType, nextType)) {
+ return true
+ }
+ return false
+}
+
+function resolveFamily(type) {
+ // Only check updated types to keep lookups fast.
+ return updatedFamiliesByType.get(type)
+}
+
+// This is a safety mechanism to protect against rogue getters and Proxies.
+function getProperty(object, property) {
+ try {
+ return object[property]
+ } catch (err) {
+ // Intentionally ignore.
+ return undefined
+ }
+}
+
+function performReactRefresh() {
+ if (pendingUpdates.length === 0) {
+ return null
+ }
+ if (isPerformingRefresh) {
+ return null
+ }
+
+ isPerformingRefresh = true
+ try {
+ const staleFamilies = new Set()
+ const updatedFamilies = new Set()
+
+ const updates = pendingUpdates
+ pendingUpdates = []
+ updates.forEach(([family, nextType]) => {
+ // Now that we got a real edit, we can create associations
+ // that will be read by the React reconciler.
+ const prevType = family.current
+ updatedFamiliesByType.set(prevType, family)
+ updatedFamiliesByType.set(nextType, family)
+ family.current = nextType
+
+ // Determine whether this should be a re-render or a re-mount.
+ if (canPreserveStateBetween(prevType, nextType)) {
+ updatedFamilies.add(family)
+ } else {
+ staleFamilies.add(family)
+ }
+ })
+
+ // TODO: rename these fields to something more meaningful.
+ const update = {
+ updatedFamilies, // Families that will re-render preserving state
+ staleFamilies, // Families that will be remounted
+ }
+
+ helpersByRendererID.forEach((helpers) => {
+ // Even if there are no roots, set the handler on first update.
+ // This ensures that if *new* roots are mounted, they'll use the resolve handler.
+ helpers.setRefreshHandler(resolveFamily)
+ })
+
+ let didError = false
+ let firstError = null
+
+ // We snapshot maps and sets that are mutated during commits.
+ // If we don't do this, there is a risk they will be mutated while
+ // we iterate over them. For example, trying to recover a failed root
+ // may cause another root to be added to the failed list -- an infinite loop.
+ const failedRootsSnapshot = new Set(failedRoots)
+ const mountedRootsSnapshot = new Set(mountedRoots)
+ const helpersByRootSnapshot = new Map(helpersByRoot)
+
+ failedRootsSnapshot.forEach((root) => {
+ const helpers = helpersByRootSnapshot.get(root)
+ if (helpers === undefined) {
+ throw new Error(
+ 'Could not find helpers for a root. This is a bug in React Refresh.',
+ )
+ }
+ if (!failedRoots.has(root)) {
+ // No longer failed.
+ }
+ if (rootElements === null) {
+ return
+ }
+ if (!rootElements.has(root)) {
+ return
+ }
+ const element = rootElements.get(root)
+ try {
+ helpers.scheduleRoot(root, element)
+ } catch (err) {
+ if (!didError) {
+ didError = true
+ firstError = err
+ }
+ // Keep trying other roots.
+ }
+ })
+ mountedRootsSnapshot.forEach((root) => {
+ const helpers = helpersByRootSnapshot.get(root)
+ if (helpers === undefined) {
+ throw new Error(
+ 'Could not find helpers for a root. This is a bug in React Refresh.',
+ )
+ }
+ if (!mountedRoots.has(root)) {
+ // No longer mounted.
+ }
+ try {
+ helpers.scheduleRefresh(root, update)
+ } catch (err) {
+ if (!didError) {
+ didError = true
+ firstError = err
+ }
+ // Keep trying other roots.
+ }
+ })
+ if (didError) {
+ throw firstError
+ }
+ return update
+ } finally {
+ isPerformingRefresh = false
+ }
+}
+
+export function register(type, id) {
+ if (type === null) {
+ return
+ }
+ if (typeof type !== 'function' && typeof type !== 'object') {
+ return
+ }
+
+ // This can happen in an edge case, e.g. if we register
+ // return value of a HOC but it returns a cached component.
+ // Ignore anything but the first registration for each type.
+ if (allFamiliesByType.has(type)) {
+ return
+ }
+ // Create family or remember to update it.
+ // None of this bookkeeping affects reconciliation
+ // until the first performReactRefresh() call above.
+ let family = allFamiliesByID.get(id)
+ if (family === undefined) {
+ family = { current: type }
+ allFamiliesByID.set(id, family)
+ } else {
+ pendingUpdates.push([family, type])
+ }
+ allFamiliesByType.set(type, family)
+
+ // Visit inner types because we might not have registered them.
+ if (typeof type === 'object' && type !== null) {
+ switch (getProperty(type, '$$typeof')) {
+ case REACT_FORWARD_REF_TYPE:
+ register(type.render, id + '$render')
+ break
+ case REACT_MEMO_TYPE:
+ register(type.type, id + '$type')
+ break
+ }
+ }
+}
+
+function setSignature(type, key, forceReset, getCustomHooks) {
+ if (!allSignaturesByType.has(type)) {
+ allSignaturesByType.set(type, {
+ forceReset,
+ ownKey: key,
+ fullKey: null,
+ getCustomHooks: getCustomHooks || (() => []),
+ })
+ }
+ // Visit inner types because we might not have signed them.
+ if (typeof type === 'object' && type !== null) {
+ switch (getProperty(type, '$$typeof')) {
+ case REACT_FORWARD_REF_TYPE:
+ setSignature(type.render, key, forceReset, getCustomHooks)
+ break
+ case REACT_MEMO_TYPE:
+ setSignature(type.type, key, forceReset, getCustomHooks)
+ break
+ }
+ }
+}
+
+// This is lazily called during first render for a type.
+// It captures Hook list at that time so inline requires don't break comparisons.
+function collectCustomHooksForSignature(type) {
+ const signature = allSignaturesByType.get(type)
+ if (signature !== undefined) {
+ computeFullKey(signature)
+ }
+}
+
+export function injectIntoGlobalHook(globalObject) {
+ // For React Native, the global hook will be set up by require('react-devtools-core').
+ // That code will run before us. So we need to monkeypatch functions on existing hook.
+
+ // For React Web, the global hook will be set up by the extension.
+ // This will also run before us.
+ let hook = globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__
+ if (hook === undefined) {
+ // However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
+ // Note that in this case it's important that renderer code runs *after* this method call.
+ // Otherwise, the renderer will think that there is no global hook, and won't do the injection.
+ let nextID = 0
+ globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
+ renderers: new Map(),
+ supportsFiber: true,
+ inject: (injected) => nextID++,
+ onScheduleFiberRoot: (id, root, children) => {},
+ onCommitFiberRoot: (id, root, maybePriorityLevel, didError) => {},
+ onCommitFiberUnmount() {},
+ }
+ }
+
+ if (hook.isDisabled) {
+ // This isn't a real property on the hook, but it can be set to opt out
+ // of DevTools integration and associated warnings and logs.
+ // Using console['warn'] to evade Babel and ESLint
+ console['warn'](
+ 'Something has shimmed the React DevTools global hook (__REACT_DEVTOOLS_GLOBAL_HOOK__). ' +
+ 'Fast Refresh is not compatible with this shim and will be disabled.',
+ )
+ return
+ }
+
+ // Here, we just want to get a reference to scheduleRefresh.
+ const oldInject = hook.inject
+ hook.inject = function (injected) {
+ const id = oldInject.apply(this, arguments)
+ if (
+ typeof injected.scheduleRefresh === 'function' &&
+ typeof injected.setRefreshHandler === 'function'
+ ) {
+ // This version supports React Refresh.
+ helpersByRendererID.set(id, injected)
+ }
+ return id
+ }
+
+ // Do the same for any already injected roots.
+ // This is useful if ReactDOM has already been initialized.
+ // https://github.com/facebook/react/issues/17626
+ hook.renderers.forEach((injected, id) => {
+ if (
+ typeof injected.scheduleRefresh === 'function' &&
+ typeof injected.setRefreshHandler === 'function'
+ ) {
+ // This version supports React Refresh.
+ helpersByRendererID.set(id, injected)
+ }
+ })
+
+ // We also want to track currently mounted roots.
+ const oldOnCommitFiberRoot = hook.onCommitFiberRoot
+ const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {})
+ hook.onScheduleFiberRoot = function (id, root, children) {
+ if (!isPerformingRefresh) {
+ // If it was intentionally scheduled, don't attempt to restore.
+ // This includes intentionally scheduled unmounts.
+ failedRoots.delete(root)
+ if (rootElements !== null) {
+ rootElements.set(root, children)
+ }
+ }
+ return oldOnScheduleFiberRoot.apply(this, arguments)
+ }
+ hook.onCommitFiberRoot = function (id, root, maybePriorityLevel, didError) {
+ const helpers = helpersByRendererID.get(id)
+ if (helpers !== undefined) {
+ helpersByRoot.set(root, helpers)
+
+ const current = root.current
+ const alternate = current.alternate
+
+ // We need to determine whether this root has just (un)mounted.
+ // This logic is copy-pasted from similar logic in the DevTools backend.
+ // If this breaks with some refactoring, you'll want to update DevTools too.
+
+ if (alternate !== null) {
+ const wasMounted =
+ alternate.memoizedState != null &&
+ alternate.memoizedState.element != null &&
+ mountedRoots.has(root)
+
+ const isMounted =
+ current.memoizedState != null && current.memoizedState.element != null
+
+ if (!wasMounted && isMounted) {
+ // Mount a new root.
+ mountedRoots.add(root)
+ failedRoots.delete(root)
+ } else if (wasMounted && isMounted) {
+ // Update an existing root.
+ // This doesn't affect our mounted root Set.
+ } else if (wasMounted && !isMounted) {
+ // Unmount an existing root.
+ mountedRoots.delete(root)
+ if (didError) {
+ // We'll remount it on future edits.
+ failedRoots.add(root)
+ } else {
+ helpersByRoot.delete(root)
+ }
+ } else if (!wasMounted && !isMounted) {
+ if (didError) {
+ // We'll remount it on future edits.
+ failedRoots.add(root)
+ }
+ }
+ } else {
+ // Mount a new root.
+ mountedRoots.add(root)
+ }
+ }
+
+ // Always call the decorated DevTools hook.
+ return oldOnCommitFiberRoot.apply(this, arguments)
+ }
+}
+
+// This is a wrapper over more primitive functions for setting signature.
+// Signatures let us decide whether the Hook order has changed on refresh.
+//
+// This function is intended to be used as a transform target, e.g.:
+// var _s = createSignatureFunctionForTransform()
+//
+// function Hello() {
+// const [foo, setFoo] = useState(0);
+// const value = useCustomHook();
+// _s(); /* Call without arguments triggers collecting the custom Hook list.
+// * This doesn't happen during the module evaluation because we
+// * don't want to change the module order with inline requires.
+// * Next calls are noops. */
+// return
Hi
;
+// }
+//
+// /* Call with arguments attaches the signature to the type: */
+// _s(
+// Hello,
+// 'useState{[foo, setFoo]}(0)',
+// () => [useCustomHook], /* Lazy to avoid triggering inline requires */
+// );
+export function createSignatureFunctionForTransform() {
+ let savedType
+ let hasCustomHooks
+ let didCollectHooks = false
+ return function (type, key, forceReset, getCustomHooks) {
+ if (typeof key === 'string') {
+ // We're in the initial phase that associates signatures
+ // with the functions. Note this may be called multiple times
+ // in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))).
+ if (!savedType) {
+ // We're in the innermost call, so this is the actual type.
+ // $FlowFixMe[escaped-generic] discovered when updating Flow
+ savedType = type
+ hasCustomHooks = typeof getCustomHooks === 'function'
+ }
+ // Set the signature for all types (even wrappers!) in case
+ // they have no signatures of their own. This is to prevent
+ // problems like https://github.com/facebook/react/issues/20417.
+ if (
+ type != null &&
+ (typeof type === 'function' || typeof type === 'object')
+ ) {
+ setSignature(type, key, forceReset, getCustomHooks)
+ }
+ return type
+ } else {
+ // We're in the _s() call without arguments, which means
+ // this is the time to collect custom Hook signatures.
+ // Only do this once. This path is hot and runs *inside* every render!
+ if (!didCollectHooks && hasCustomHooks) {
+ didCollectHooks = true
+ collectCustomHooksForSignature(savedType)
+ }
+ }
+ }
+}
+
+function isLikelyComponentType(type) {
+ switch (typeof type) {
+ case 'function': {
+ // First, deal with classes.
+ if (type.prototype != null) {
+ if (type.prototype.isReactComponent) {
+ // React class.
+ return true
+ }
+ const ownNames = Object.getOwnPropertyNames(type.prototype)
+ if (ownNames.length > 1 || ownNames[0] !== 'constructor') {
+ // This looks like a class.
+ return false
+ }
+
+ if (type.prototype.__proto__ !== Object.prototype) {
+ // It has a superclass.
+ return false
+ }
+ // Pass through.
+ // This looks like a regular function with empty prototype.
+ }
+ // For plain functions and arrows, use name as a heuristic.
+ const name = type.name || type.displayName
+ return typeof name === 'string' && /^[A-Z]/.test(name)
+ }
+ case 'object': {
+ if (type != null) {
+ switch (getProperty(type, '$$typeof')) {
+ case REACT_FORWARD_REF_TYPE:
+ case REACT_MEMO_TYPE:
+ // Definitely React components.
+ return true
+ default:
+ return false
+ }
+ }
+ return false
+ }
+ default: {
+ return false
+ }
+ }
+}
+
+function isCompoundComponent(type) {
+ if (!isPlainObject(type)) return false
+ for (const key in type) {
+ if (!isLikelyComponentType(type[key])) return false
+ }
+ return true
+}
+
+function isPlainObject(obj) {
+ return (
+ Object.prototype.toString.call(obj) === '[object Object]' &&
+ (obj.constructor === Object || obj.constructor === undefined)
+ )
+}
+
+/**
+ * Plugin utils
+ */
+
+// Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141
+// This allows to resister components not detected by SWC like styled component
+export function registerExportsForReactRefresh(filename, moduleExports) {
+ for (const key in moduleExports) {
+ if (key === '__esModule') continue
+ const exportValue = moduleExports[key]
+ if (isLikelyComponentType(exportValue)) {
+ // 'export' is required to avoid key collision when renamed exports that
+ // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116
+ // The register function has an identity check to not register twice the same component,
+ // so this is safe to not used the same key here.
+ register(exportValue, filename + ' export ' + key)
+ } else if (isCompoundComponent(exportValue)) {
+ for (const subKey in exportValue) {
+ register(
+ exportValue[subKey],
+ filename + ' export ' + key + '-' + subKey,
+ )
+ }
+ }
+ }
+}
+
+function debounce(fn, delay) {
+ let handle
+ return () => {
+ clearTimeout(handle)
+ handle = setTimeout(fn, delay)
+ }
+}
+
+const hooks = []
+window.__registerBeforePerformReactRefresh = (cb) => {
+ hooks.push(cb)
+}
+const enqueueUpdate = debounce(async () => {
+ if (hooks.length) await Promise.all(hooks.map((cb) => cb()))
+ performReactRefresh()
+}, 16)
+
+export function validateRefreshBoundaryAndEnqueueUpdate(
+ id,
+ prevExports,
+ nextExports,
+) {
+ const ignoredExports = window.__getReactRefreshIgnoredExports?.({ id }) ?? []
+ if (
+ predicateOnExport(
+ ignoredExports,
+ prevExports,
+ (key) => key in nextExports,
+ ) !== true
+ ) {
+ return 'Could not Fast Refresh (export removed)'
+ }
+ if (
+ predicateOnExport(
+ ignoredExports,
+ nextExports,
+ (key) => key in prevExports,
+ ) !== true
+ ) {
+ return 'Could not Fast Refresh (new export)'
+ }
+
+ let hasExports = false
+ const allExportsAreComponentsOrUnchanged = predicateOnExport(
+ ignoredExports,
+ nextExports,
+ (key, value) => {
+ hasExports = true
+ if (isLikelyComponentType(value)) return true
+ if (isCompoundComponent(value)) return true
+ return prevExports[key] === nextExports[key]
+ },
+ )
+ if (hasExports && allExportsAreComponentsOrUnchanged === true) {
+ enqueueUpdate()
+ } else {
+ return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at __README_URL__#consistent-components-exports`
+ }
+}
+
+function predicateOnExport(ignoredExports, moduleExports, predicate) {
+ for (const key in moduleExports) {
+ if (ignoredExports.includes(key)) continue
+ if (!predicate(key, moduleExports[key])) return key
+ }
+ return true
+}
+
+// Hides vite-ignored dynamic import so that Vite can skip analysis if no other
+// dynamic import is present (https://github.com/vitejs/vite/pull/12732)
+export const __hmr_import = (module) => import(/* @vite-ignore */ module)
+
+// For backwards compatibility with @vitejs/plugin-react.
+export default { injectIntoGlobalHook }
diff --git a/packages/common/refresh-utils.ts b/packages/common/refresh-utils.ts
new file mode 100644
index 000000000..db0e587e4
--- /dev/null
+++ b/packages/common/refresh-utils.ts
@@ -0,0 +1,62 @@
+export const runtimePublicPath = '/@react-refresh'
+
+const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
+const refreshContentRE = /\$RefreshReg\$\(/
+
+// NOTE: this is exposed publicly via plugin-react
+export const preambleCode = `import { injectIntoGlobalHook } from "__BASE__${runtimePublicPath.slice(
+ 1,
+)}";
+injectIntoGlobalHook(window);
+window.$RefreshReg$ = () => {};
+window.$RefreshSig$ = () => (type) => type;`
+
+export const getPreambleCode = (base: string): string =>
+ preambleCode.replace('__BASE__', base)
+
+export function addRefreshWrapper(
+ code: string,
+ pluginName: string,
+ id: string,
+ reactRefreshHost = '',
+): string | undefined {
+ const hasRefresh = refreshContentRE.test(code)
+ const onlyReactComp = !hasRefresh && reactCompRE.test(code)
+
+ if (!hasRefresh && !onlyReactComp) return undefined
+
+ let newCode = code
+ newCode += `
+
+import * as RefreshRuntime from "${reactRefreshHost}${runtimePublicPath}";
+const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
+if (import.meta.hot && !inWebWorker) {
+ if (!window.$RefreshReg$) {
+ throw new Error(
+ "${pluginName} can't detect preamble. Something is wrong."
+ );
+ }
+
+ RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
+ RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(
+ id,
+ )}, currentExports);
+ import.meta.hot.accept((nextExports) => {
+ if (!nextExports) return;
+ const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify(
+ id,
+ )}, currentExports, nextExports);
+ if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
+ });
+ });
+}
+`
+
+ if (hasRefresh) {
+ newCode += `function $RefreshReg$(type, id) { return RefreshRuntime.register(type, ${JSON.stringify(id)} + ' ' + id) }
+function $RefreshSig$() { return RefreshRuntime.createSignatureFunctionForTransform(); }
+`
+ }
+
+ return newCode
+}
diff --git a/packages/common/warning.ts b/packages/common/warning.ts
new file mode 100644
index 000000000..85e0cf434
--- /dev/null
+++ b/packages/common/warning.ts
@@ -0,0 +1,30 @@
+import type { BuildOptions, UserConfig } from 'vite'
+
+export const silenceUseClientWarning = (
+ userConfig: UserConfig,
+): BuildOptions => ({
+ rollupOptions: {
+ onwarn(warning, defaultHandler) {
+ if (
+ warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
+ (warning.message.includes('use client') ||
+ warning.message.includes('use server'))
+ ) {
+ return
+ }
+ // https://github.com/vitejs/vite/issues/15012
+ if (
+ warning.code === 'SOURCEMAP_ERROR' &&
+ warning.message.includes('resolve original location') &&
+ warning.pos === 0
+ ) {
+ return
+ }
+ if (userConfig.build?.rollupOptions?.onwarn) {
+ userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
+ } else {
+ defaultHandler(warning)
+ }
+ },
+ },
+})
diff --git a/packages/plugin-react-oxc/CHANGELOG.md b/packages/plugin-react-oxc/CHANGELOG.md
new file mode 100644
index 000000000..1d0236e52
--- /dev/null
+++ b/packages/plugin-react-oxc/CHANGELOG.md
@@ -0,0 +1,86 @@
+# Changelog
+
+## Unreleased
+
+## 0.4.2 (2025-09-17)
+
+### Perf: simplify refresh wrapper generation ([#835](https://github.com/vitejs/vite-plugin-react/pull/835))
+
+## 0.4.1 (2025-08-19)
+
+### Set `optimizeDeps.rollupOptions.transform.jsx` instead of `optimizeDeps.rollupOptions.jsx` ([#735](https://github.com/vitejs/vite-plugin-react/pull/735))
+
+`optimizeDeps.rollupOptions.jsx` is going to be deprecated in favor of `optimizeDeps.rollupOptions.transform.jsx`.
+
+## 0.4.0 (2025-08-07)
+
+## 0.4.0-beta.0 (2025-07-28)
+
+### Deprecate this plugin
+
+The changes of this plugin is now included in `@vitejs/plugin-react`. Please use `@vitejs/plugin-react` instead.
+
+### Allow processing files in `node_modules`
+
+The default value of `exclude` options is now `[/\/node_modules\//]` to allow processing files in `node_modules` directory. It was previously `[]` and files in `node_modules` was always excluded regardless of the value of `exclude` option.
+
+### Require Node 20.19+, 22.12+
+
+This plugin now requires Node 20.19+ or 22.12+.
+
+## 0.3.0 (2025-07-18)
+
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () =>
Accordion Root
+const Item = () =>
Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
+### Return `Plugin[]` instead of `PluginOption[]` ([#537](https://github.com/vitejs/vite-plugin-react/pull/537))
+
+The return type has changed from `react(): PluginOption[]` to more specialized type `react(): Plugin[]`. This allows for type-safe manipulation of plugins, for example:
+
+```tsx
+// previously this causes type errors
+react()
+ .map(p => ({ ...p, applyToEnvironment: e => e.name === 'client' }))
+```
+
+## 0.2.3 (2025-06-16)
+
+### Disable refresh transform when `server.hmr: false` is set [#502](https://github.com/vitejs/vite-plugin-react/pull/502)
+
+This fixes "`$RefreshReg$` is not defined" error when running Vitest with the plugin.
+
+## 0.2.2 (2025-06-10)
+
+### Add Vite 7-beta to peerDependencies range [#497](https://github.com/vitejs/vite-plugin-react/pull/497)
+
+React plugins are compatible with Vite 7, this removes the warning when testing the beta.
+
+## 0.2.1 (2025-06-03)
+
+### Add explicit semicolon in preambleCode [#485](https://github.com/vitejs/vite-plugin-react/pull/485)
+
+This fixes an edge case when using HTML minifiers that strips line breaks aggressively.
+
+## 0.2.0 (2025-05-23)
+
+### Add `filter` for rolldown-vite [#470](https://github.com/vitejs/vite-plugin-react/pull/470)
+
+Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite.
+
+### Skip HMR for JSX files with hooks [#480](https://github.com/vitejs/vite-plugin-react/pull/480)
+
+This removes the HMR warning for hooks with JSX.
+
+## 0.1.1 (2025-04-10)
+
+## 0.1.0 (2025-04-09)
+
+- Create Oxc plugin
diff --git a/packages/plugin-react-oxc/LICENSE b/packages/plugin-react-oxc/LICENSE
new file mode 100644
index 000000000..9c1b313d7
--- /dev/null
+++ b/packages/plugin-react-oxc/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/plugin-react-oxc/README.md b/packages/plugin-react-oxc/README.md
new file mode 100644
index 000000000..bc6b77646
--- /dev/null
+++ b/packages/plugin-react-oxc/README.md
@@ -0,0 +1,85 @@
+> [!IMPORTANT]
+> This package is deprecated. Please use [@vitejs/plugin-react](https://www.npmjs.com/package/@vitejs/plugin-react) instead, which automatically enables Oxc-based Fast Refresh transform on [`rolldown-vite`](https://vitejs.dev/guide/rolldown).
+
+# @vitejs/plugin-react-oxc [](https://npmjs.com/package/@vitejs/plugin-react-oxc)
+
+The future default Vite plugin for React projects.
+
+- enable [Fast Refresh](https://www.npmjs.com/package/react-refresh) in development
+- use the [automatic JSX runtime](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)
+- small installation size
+
+```js
+// vite.config.js
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-oxc'
+
+export default defineConfig({
+ plugins: [react()],
+})
+```
+
+## Caveats
+
+- `jsx runtime` is always `automatic`
+- this plugin only works with [`rolldown-vite`](https://vitejs.dev/guide/rolldown)
+
+## Options
+
+### include/exclude
+
+Includes `.js`, `.jsx`, `.ts` & `.tsx` and excludes `/node_modules/` by default. This option can be used to add fast refresh to `.mdx` files:
+
+```js
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import mdx from '@mdx-js/rollup'
+
+export default defineConfig({
+ plugins: [
+ { enforce: 'pre', ...mdx() },
+ react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }),
+ ],
+})
+```
+
+### jsxImportSource
+
+Control where the JSX factory is imported from. Default to `'react'`
+
+```js
+react({ jsxImportSource: '@emotion/react' })
+```
+
+## Middleware mode
+
+In [middleware mode](https://vite.dev/config/server-options.html#server-middlewaremode), you should make sure your entry `index.html` file is transformed by Vite. Here's an example for an Express server:
+
+```js
+app.get('/', async (req, res, next) => {
+ try {
+ let html = fs.readFileSync(path.resolve(root, 'index.html'), 'utf-8')
+
+ // Transform HTML using Vite plugins.
+ html = await viteServer.transformIndexHtml(req.url, html)
+
+ res.send(html)
+ } catch (e) {
+ return next(e)
+ }
+})
+```
+
+Otherwise, you'll probably get this error:
+
+```
+Uncaught Error: @vitejs/plugin-react-oxc can't detect preamble. Something is wrong.
+```
+
+## Consistent components exports
+
+For React refresh to work correctly, your file should only export React components. You can find a good explanation in the [Gatsby docs](https://www.gatsbyjs.com/docs/reference/local-development/fast-refresh/#how-it-works).
+
+If an incompatible change in exports is found, the module will be invalidated and HMR will propagate. To make it easier to export simple constants alongside your component, the module is only invalidated when their value changes.
+
+You can catch mistakes and get more detailed warning with this [eslint rule](https://github.com/ArnaudBarre/eslint-plugin-react-refresh).
diff --git a/packages/plugin-react-oxc/package.json b/packages/plugin-react-oxc/package.json
new file mode 100644
index 000000000..04539f2b7
--- /dev/null
+++ b/packages/plugin-react-oxc/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@vitejs/plugin-react-oxc",
+ "version": "0.4.2",
+ "license": "MIT",
+ "author": "Evan You",
+ "contributors": [
+ "Alec Larson",
+ "Arnaud Barré"
+ ],
+ "description": "The future default Vite plugin for React projects",
+ "keywords": [
+ "vite",
+ "vite-plugin",
+ "react",
+ "oxc",
+ "react-refresh",
+ "fast refresh"
+ ],
+ "files": [
+ "dist"
+ ],
+ "type": "module",
+ "exports": "./dist/index.js",
+ "scripts": {
+ "dev": "tsdown --watch ./src --watch ../common",
+ "build": "tsdown",
+ "prepublishOnly": "npm run build"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vitejs/vite-plugin-react.git",
+ "directory": "packages/plugin-react-oxc"
+ },
+ "bugs": {
+ "url": "https://github.com/vitejs/vite-plugin-react/issues"
+ },
+ "homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme",
+ "peerDependencies": {
+ "vite": "^6.3.0 || ^7.0.0"
+ },
+ "devDependencies": {
+ "@vitejs/react-common": "workspace:*",
+ "tsdown": "^0.15.6",
+ "vite": "catalog:rolldown-vite"
+ },
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.41"
+ }
+}
diff --git a/packages/plugin-react-oxc/src/index.ts b/packages/plugin-react-oxc/src/index.ts
new file mode 100644
index 000000000..e42b7356a
--- /dev/null
+++ b/packages/plugin-react-oxc/src/index.ts
@@ -0,0 +1,159 @@
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { readFileSync } from 'node:fs'
+import type { BuildOptions, Plugin } from 'vite'
+import {
+ addRefreshWrapper,
+ getPreambleCode,
+ runtimePublicPath,
+ silenceUseClientWarning,
+} from '@vitejs/react-common'
+import { exactRegex } from '@rolldown/pluginutils'
+
+const _dirname = dirname(fileURLToPath(import.meta.url))
+const refreshRuntimePath = join(_dirname, 'refresh-runtime.js')
+
+export interface Options {
+ include?: string | RegExp | Array
+ exclude?: string | RegExp | Array
+ /**
+ * Control where the JSX factory is imported from.
+ * @default 'react'
+ */
+ jsxImportSource?: string
+}
+
+const defaultIncludeRE = /\.[tj]sx?(?:$|\?)/
+const defaultExcludeRE = /\/node_modules\//
+
+export default function viteReact(opts: Options = {}): Plugin[] {
+ const include = opts.include ?? defaultIncludeRE
+ const exclude = opts.exclude ?? defaultExcludeRE
+
+ const jsxImportSource = opts.jsxImportSource ?? 'react'
+ const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
+ const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`
+
+ const viteConfig: Plugin = {
+ name: 'vite:react-oxc:config',
+ config(userConfig, { command }) {
+ return {
+ // @ts-expect-error rolldown-vite Vite type incompatibility
+ build: silenceUseClientWarning(userConfig) as BuildOptions,
+ oxc: {
+ jsx: {
+ runtime: 'automatic',
+ importSource: jsxImportSource,
+ refresh: command === 'serve',
+ development: command === 'serve',
+ },
+ jsxRefreshInclude: include,
+ jsxRefreshExclude: exclude,
+ },
+ optimizeDeps: {
+ include: [
+ 'react',
+ 'react-dom',
+ jsxImportDevRuntime,
+ jsxImportRuntime,
+ ],
+ rollupOptions: { transform: { jsx: { runtime: 'automatic' } } },
+ },
+ }
+ },
+ configResolved(config) {
+ config.logger.warn(
+ '@vitejs/plugin-react-oxc is deprecated. ' +
+ 'Please use @vitejs/plugin-react instead. ' +
+ 'The changes of this plugin is now included in @vitejs/plugin-react.',
+ )
+ },
+ options() {
+ if (!this.meta.rolldownVersion) {
+ throw new Error(
+ '@vitejs/plugin-react-oxc requires rolldown-vite to be used. ' +
+ 'See https://vitejs.dev/guide/rolldown for more details about rolldown-vite.',
+ )
+ }
+ },
+ }
+
+ const viteConfigPost: Plugin = {
+ name: 'vite:react-oxc:config-post',
+ enforce: 'post',
+ config(userConfig) {
+ if (userConfig.server?.hmr === false) {
+ return {
+ oxc: {
+ jsx: {
+ refresh: false,
+ },
+ },
+ }
+ }
+ },
+ }
+
+ const viteRefreshRuntime: Plugin = {
+ name: 'vite:react-oxc:refresh-runtime',
+ enforce: 'pre',
+ resolveId: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler(id) {
+ return id
+ },
+ },
+ load: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler(_id) {
+ return readFileSync(refreshRuntimePath, 'utf-8').replace(
+ /__README_URL__/g,
+ 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-oxc',
+ )
+ },
+ },
+ }
+
+ let skipFastRefresh = false
+
+ const viteRefreshWrapper: Plugin = {
+ name: 'vite:react-oxc:refresh-wrapper',
+ apply: 'serve',
+ configResolved(config) {
+ skipFastRefresh = config.isProduction || config.server.hmr === false
+ },
+ transform: {
+ filter: {
+ id: { include, exclude },
+ },
+ handler(code, id, options) {
+ const ssr = options?.ssr === true
+
+ const [filepath] = id.split('?')
+ const isJSX = filepath.endsWith('x')
+ const useFastRefresh =
+ !skipFastRefresh &&
+ !ssr &&
+ (isJSX ||
+ code.includes(jsxImportDevRuntime) ||
+ code.includes(jsxImportRuntime))
+ if (!useFastRefresh) return
+
+ const newCode = addRefreshWrapper(code, '@vitejs/plugin-react-oxc', id)
+ return newCode ? { code: newCode, map: null } : undefined
+ },
+ },
+ transformIndexHtml(_, config) {
+ if (!skipFastRefresh)
+ return [
+ {
+ tag: 'script',
+ attrs: { type: 'module' },
+ children: getPreambleCode(config.server!.config.base),
+ },
+ ]
+ },
+ }
+
+ return [viteConfig, viteConfigPost, viteRefreshRuntime, viteRefreshWrapper]
+}
diff --git a/packages/plugin-react-oxc/tsconfig.json b/packages/plugin-react-oxc/tsconfig.json
new file mode 100644
index 000000000..70c7eacff
--- /dev/null
+++ b/packages/plugin-react-oxc/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "dist",
+ "target": "es2023",
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "declaration": true,
+ "sourceMap": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "esModuleInterop": true
+ }
+}
diff --git a/packages/plugin-react-oxc/tsdown.config.ts b/packages/plugin-react-oxc/tsdown.config.ts
new file mode 100644
index 000000000..3e38aa5d7
--- /dev/null
+++ b/packages/plugin-react-oxc/tsdown.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'tsdown'
+
+export default defineConfig({
+ entry: 'src/index.ts',
+ dts: true,
+ copy: [
+ {
+ from: 'node_modules/@vitejs/react-common/refresh-runtime.js',
+ to: 'dist/refresh-runtime.js',
+ },
+ ],
+})
diff --git a/packages/plugin-react-swc/CHANGELOG.md b/packages/plugin-react-swc/CHANGELOG.md
new file mode 100644
index 000000000..532ef9ed2
--- /dev/null
+++ b/packages/plugin-react-swc/CHANGELOG.md
@@ -0,0 +1,322 @@
+# Changelog
+
+## Unreleased
+
+## 4.1.0 (2025-09-17)
+
+### Set SWC cacheRoot options
+
+This is set to `{viteCacheDir}/swc` and override the default of `.swc`.
+
+### Perf: simplify refresh wrapper generation ([#835](https://github.com/vitejs/vite-plugin-react/pull/835))
+
+## 4.0.1 (2025-08-19)
+
+### Set `optimizeDeps.rollupOptions.transform.jsx` instead of `optimizeDeps.rollupOptions.jsx` for rolldown-vite ([#735](https://github.com/vitejs/vite-plugin-react/pull/735))
+
+`optimizeDeps.rollupOptions.jsx` is going to be deprecated in favor of `optimizeDeps.rollupOptions.transform.jsx`.
+
+## 4.0.0 (2025-08-07)
+
+## 4.0.0-beta.0 (2025-07-28)
+
+### Require Node 20.19+, 22.12+
+
+This plugin now requires Node 20.19+ or 22.12+.
+
+## 3.11.0 (2025-07-18)
+
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () =>
Accordion Root
+const Item = () =>
Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
+### Return `Plugin[]` instead of `PluginOption[]` ([#537](https://github.com/vitejs/vite-plugin-react/pull/537))
+
+The return type has changed from `react(): PluginOption[]` to more specialized type `react(): Plugin[]`. This allows for type-safe manipulation of plugins, for example:
+
+```tsx
+// previously this causes type errors
+react()
+ .map(p => ({ ...p, applyToEnvironment: e => e.name === 'client' }))
+```
+
+## 3.10.2 (2025-06-10)
+
+### Suggest `@vitejs/plugin-react-oxc` if rolldown-vite is detected [#491](https://github.com/vitejs/vite-plugin-react/pull/491)
+
+Emit a log which recommends `@vitejs/plugin-react-oxc` when `rolldown-vite` is detected to improve performance and use Oxc under the hood. The warning can be disabled by setting `disableOxcRecommendation: true` in the plugin options.
+
+### Use `optimizeDeps.rollupOptions` instead of `optimizeDeps.esbuildOptions` for rolldown-vite [#489](https://github.com/vitejs/vite-plugin-react/pull/489)
+
+This suppresses the warning about `optimizeDeps.esbuildOptions` being deprecated in rolldown-vite.
+
+### Add Vite 7-beta to peerDependencies range [#497](https://github.com/vitejs/vite-plugin-react/pull/497)
+
+React plugins are compatible with Vite 7, this removes the warning when testing the beta.
+
+## 3.10.1 (2025-06-03)
+
+### Add explicit semicolon in preambleCode [#485](https://github.com/vitejs/vite-plugin-react/pull/485)
+
+This fixes an edge case when using HTML minifiers that strips line breaks aggressively.
+
+## 3.10.0 (2025-05-23)
+
+### Add `filter` for rolldown-vite [#470](https://github.com/vitejs/vite-plugin-react/pull/470)
+
+Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite.
+
+### Skip HMR preamble in Vitest browser mode [#478](https://github.com/vitejs/vite-plugin-react/pull/478)
+
+This was causing annoying `Sourcemap for "/@react-refresh" points to missing source files` and is unnecessary in test mode.
+
+### Skip HMR for JSX files with hooks [#480](https://github.com/vitejs/vite-plugin-react/pull/480)
+
+This removes the HMR warning for hooks with JSX.
+
+## 3.9.0 (2025-04-15)
+
+### Make compatible with rolldown-vite
+
+This plugin is now compatible with rolldown-powered version of Vite.
+
+## 3.9.0-beta.3 (2025-04-15)
+
+### Add `reactRefreshHost` option
+
+Add `reactRefreshHost` option to set a React Fast Refresh runtime URL prefix.
+This is useful in a module federation context to enable HMR by specifying the host application URL in the Vite config of a remote application.
+See full discussion here: https://github.com/module-federation/vite/issues/183#issuecomment-2751825367
+
+```ts
+export default defineConfig({
+ plugins: [react({ reactRefreshHost: 'http://localhost:3000' })],
+})
+```
+
+## 3.9.0-beta.2 (2025-04-09)
+
+## 3.9.0-beta.0 (2025-04-09)
+
+## 3.8.1
+
+### Remove WebContainers warning [#268](https://github.com/vitejs/vite-plugin-react-swc/pull/268)
+
+SWC is now supported in WebContainers 🎉
+
+## 3.8.0
+
+### Add useAtYourOwnRisk_mutateSwcOptions option
+
+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.
+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`.
+
+```ts
+react({
+ useAtYourOwnRisk_mutateSwcOptions(options) {
+ options.jsc.parser.decorators = true;
+ options.jsc.transform.decoratorVersion = "2022-03";
+ },
+});
+```
+
+## 3.7.2
+
+### Add Vite 6 to peerDependencies range [#207](https://github.com/vitejs/vite-plugin-react-swc/pull/207)
+
+Thanks @RobinTail
+
+### Revert throw when refresh runtime is loaded twice [#237](https://github.com/vitejs/vite-plugin-react-swc/issues/237)
+
+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.
+
+This revert was done in the Babel plugin last year and I didn't port it back.
+
+## 3.7.1
+
+Ignore directive sourcemap error [#231](https://github.com/vitejs/vite-plugin-react-swc/issues/231)
+
+## 3.7.0
+
+### Support HMR for class components
+
+This is a long overdue and should fix some issues people had with HMR when migrating from CRA.
+
+## 3.6.0
+
+### Add parserConfig option
+
+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.
+
+## 3.5.0
+
+### Update peer dependency range to target Vite 5
+
+There were no breaking change that impacted this plugin, so any combination of React plugins and Vite core version will work.
+
+### Align jsx runtime for optimized dependencies
+
+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.
+
+Reminder: While being partially supported in Vite, publishing TS & JSX outside of internal libraries is highly discouraged.
+
+## 3.4.1
+
+### Add support for `.mts` (fixes [#161](https://github.com/vitejs/vite-plugin-react-swc/issues/161))
+
+Using CJS in source code will not work in Vite (and will never be supported), so this is better to only use `.ts`.
+
+But to better align with [Vite core defaults](https://vite.dev/config/shared-options.html#resolve-extensions), `.mts` extension will now be processed like `.ts`. This maybe reverted in a future major.
+
+## 3.4.0
+
+- Add `devTarget` option (fixes [#141](https://github.com/vitejs/vite-plugin-react-swc/issues/141))
+- Disable Fast Refresh based on `config.server.hmr === false` instead of `process.env.TEST`
+- Warn when plugin is in WebContainers (see [#118](https://github.com/vitejs/vite-plugin-react-swc/issues/118))
+- 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))
+
+## 3.3.2
+
+- Support [Vitest deps.experimentalOptimizer](https://vitest.dev/config/#deps-experimentaloptimizer) ([#115](https://github.com/vitejs/vite-plugin-react-swc/pull/115))
+
+## 3.3.1
+
+- 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))
+- 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.
+
+## 3.3.0
+
+- 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)
+- Silence `"use client"` warning when building library like `@tanstack/react-query`
+- Fix fast refresh issue when exporting a component with a name that shadow another local component
+
+This release goes in hand with the upcoming Vite 4.3 release focusing on performances:
+
+- Move resolve of runtime code into a "pre" plugin ([#79](https://github.com/vitejs/vite-plugin-react-swc/pull/79))
+- Wrap dynamic import to speedup analysis ([#80](https://github.com/vitejs/vite-plugin-react-swc/pull/80))
+
+## 3.2.0
+
+- Support HMR for MDX (fixes #52)
+- Fix: when using plugins, apply SWC before esbuild so that automatic runtime is respected for JSX (fixes #56)
+- Fix: use jsxImportSource in optimizeDeps
+
+## 3.1.0
+
+- Support plugins via the new `plugins` options
+- Support TypeScript decorators via the new `tsDecorators` option. This requires `experimentalDecorators` in tsconfig.
+- Fix HMR for styled components exported alongside other components
+- Update embedded refresh runtime to 0.14 (fixes [#46](https://github.com/vitejs/vite-plugin-react-swc/issues/46))
+
+## 3.0.1
+
+- Support Emotion via the new `jsxImportSource` option (fixes [#25](https://github.com/vitejs/vite-plugin-react-swc/issues/25))
+
+To use it with Emotion, update your config to:
+
+```ts
+export default defineConfig({
+ plugins: [react({ jsxImportSource: "@emotion/react" })],
+});
+```
+
+- Fix HMR when using Vite `base` option (fixes [#18](https://github.com/vitejs/vite-plugin-react-swc/issues/18))
+- Fix usage with workers (fixes [#23](https://github.com/vitejs/vite-plugin-react-swc/issues/23))
+- Fix usage with `Vite Ruby` and `Laravel Vite` ([#20](https://github.com/vitejs/vite-plugin-react-swc/pull/20))
+- Fix plugin default export when using commonjs (fixes [#14](https://github.com/vitejs/vite-plugin-react-swc/issues/14))
+
+## 3.0.0
+
+This is plugin is now stable! 🎉
+
+To migrate from `vite-plugin-swc-react-refresh`, see the `3.0.0-beta.0` changelog.
+
+## 3.0.0-beta.2
+
+- breaking: update plugin name to `vite:react-swc` to match official plugins naming
+- fix: don't add React Refresh wrapper for SSR transform (fixes [#11](https://github.com/vitejs/vite-plugin-react-swc/issues/11))
+
+## 3.0.0-beta.1
+
+Fix package.json exports fields
+
+## 3.0.0-beta.0
+
+This is the first beta version of the official plugin for using [SWC](https://swc.rs/) with React in Vite!
+
+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, ...):
+
+- Automatically enable automatic JSX runtime. "classic" runtime is not supported
+- Skip transformation for `.js` files
+- Enforce [useDefineForClassFields](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier)
+- Don't pass `esbuild.define` config option to SWC. You can use the [top level define option](https://vite.dev/config/shared-options.html#define) instead
+- Use default export
+
+To migrate, change your config to:
+
+```js
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+
+export default defineConfig({
+ plugins: [react()],
+});
+```
+
+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.
+
+## 2.2.1
+
+Skip react-refresh on SSR (Fixes [#2](https://github.com/vitejs/vite-plugin-react-swc/issues/2))
+
+## 2.2.0
+
+- 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)
+- Extract line and column in SWC errors to make overlay filename clickable
+- Fix plugin name (`react-refresh` -> `swc-react-refresh`)
+
+## 2.1.0
+
+Add source maps support
+
+## 2.0.3
+
+Include `react/jsx-dev-runtime` for dependencies optimisation when using automatic runtime.
+
+## 2.0.2
+
+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)
+
+## 2.0.1
+
+Fix esbuild property in documentation.
+
+## 2.0.0
+
+Breaking: Use named export instead of default export for better esm/cjs interop.
+
+To migrate, replace your import by `import { swcReactRefresh } from "vite-plugin-swc-react-refresh";`
+
+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).
+
+To use it, update your config from `esbuild: { jsxInject: 'import React from "react"' },` to `esbuild: { jsx: "automatic" },`
+
+## 0.1.2
+
+- Add vite as peer dependency
+- Pin @swc/core version to 1.2.141 to avoid a 30mb bump of bundle size
+
+## 0.1.1
+
+Add LICENSE
+
+## 0.1.0
+
+Initial release
diff --git a/packages/plugin-react-swc/LICENSE b/packages/plugin-react-swc/LICENSE
new file mode 100644
index 000000000..75488f19b
--- /dev/null
+++ b/packages/plugin-react-swc/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Arnaud Barré (https://github.com/ArnaudBarre)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/plugin-react-swc/README.md b/packages/plugin-react-swc/README.md
new file mode 100644
index 000000000..c74f11d72
--- /dev/null
+++ b/packages/plugin-react-swc/README.md
@@ -0,0 +1,134 @@
+# @vitejs/plugin-react-swc [](https://www.npmjs.com/package/@vitejs/plugin-react-swc)
+
+Speed up your Vite dev server with [SWC](https://swc.rs/)
+
+- ✅ A fast Fast Refresh (~20x faster than Babel)
+- ✅ Enable [automatic JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)
+
+## Installation
+
+```sh
+npm i -D @vitejs/plugin-react-swc
+```
+
+## Usage
+
+```ts
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react()],
+})
+```
+
+## Caveats
+
+This plugin has limited options to enable good performances and be transpiler agnostic. Here is the list of non-configurable options that impact runtime behaviour:
+
+- [useDefineForClassFields](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier) is always activated, as this matches the current ECMAScript spec
+- `jsx runtime` is always `automatic`
+- In development:
+ - esbuild is disabled, so the [esbuild configuration](https://vite.dev/config/shared-options.html#esbuild) has no effect
+ - `target` is ignored and defaults to `es2020` (see [`devTarget`](#devtarget))
+ - JS files are not transformed
+ - tsconfig is not resolved, so properties other than the ones listed above behaves like TS defaults
+
+## Options
+
+### jsxImportSource
+
+Control where the JSX factory is imported from.
+
+`@default` "react"
+
+```ts
+react({ jsxImportSource: '@emotion/react' })
+```
+
+### tsDecorators
+
+Enable TypeScript decorators. Requires `experimentalDecorators` in tsconfig.
+
+`@default` false
+
+```ts
+react({ tsDecorators: true })
+```
+
+### plugins
+
+Use SWC plugins. Enable SWC at build time.
+
+```ts
+react({ plugins: [['@swc/plugin-styled-components', {}]] })
+```
+
+### devTarget
+
+Set the target for SWC in dev. This can avoid to down-transpile private class method for example.
+
+For production target, see https://vite.dev/config/build-options.html#build-target.
+
+`@default` "es2020"
+
+```ts
+react({ devTarget: 'es2022' })
+```
+
+### parserConfig
+
+Override the default include list (.ts, .tsx, .mts, .jsx, .mdx).
+
+This requires to redefine the config for any file you want to be included (ts, mdx, ...).
+
+If you want to trigger fast refresh on compiled JS, use `jsx: true`. Exclusion of node_modules should be handled by the function if needed. Using this option to use JSX inside `.js` files is highly discouraged and can be removed in any future version.
+
+```ts
+react({
+ parserConfig(id) {
+ if (id.endsWith('.res')) return { syntax: 'ecmascript', jsx: true }
+ if (id.endsWith('.ts')) return { syntax: 'typescript', tsx: false }
+ },
+})
+```
+
+### reactRefreshHost
+
+The `reactRefreshHost` option is only necessary in a module federation context. It enables HMR to work between a remote & host application. In your remote Vite config, you would add your host origin:
+
+```js
+react({ reactRefreshHost: 'http://localhost:3000' })
+```
+
+Under the hood, this simply updates the React Fash Refresh runtime URL from `/@react-refresh` to `http://localhost:3000/@react-refresh` to ensure there is only one Refresh runtime across the whole application. Note that if you define `base` option in the host application, you need to include it in the option, like: `http://localhost:3000/{base}`.
+
+### useAtYourOwnRisk_mutateSwcOptions
+
+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.
+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`.
+
+```ts
+react({
+ useAtYourOwnRisk_mutateSwcOptions(options) {
+ options.jsc.parser.decorators = true
+ options.jsc.transform.decoratorVersion = '2022-03'
+ },
+})
+```
+
+### disableOxcRecommendation
+
+If set, disables the recommendation to use `@vitejs/plugin-react-oxc` (which is shown when `rolldown-vite` is detected and neither `swc` plugins are used nor the `swc` options are mutated).
+
+```ts
+react({ disableOxcRecommendation: true })
+```
+
+## Consistent components exports
+
+For React refresh to work correctly, your file should only export React components. The best explanation I've read is the one from the [Gatsby docs](https://www.gatsbyjs.com/docs/reference/local-development/fast-refresh/#how-it-works).
+
+If an incompatible change in exports is found, the module will be invalidated and HMR will propagate. To make it easier to export simple constants alongside your component, the module is only invalidated when their value changes.
+
+You can catch mistakes and get more detailed warning with this [eslint rule](https://github.com/ArnaudBarre/eslint-plugin-react-refresh).
diff --git a/packages/plugin-react-swc/package.json b/packages/plugin-react-swc/package.json
new file mode 100644
index 000000000..880a7ba51
--- /dev/null
+++ b/packages/plugin-react-swc/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@vitejs/plugin-react-swc",
+ "version": "4.1.0",
+ "license": "MIT",
+ "author": "Arnaud Barré (https://github.com/ArnaudBarre)",
+ "description": "Speed up your Vite dev server with SWC",
+ "keywords": [
+ "vite",
+ "vite-plugin",
+ "react",
+ "swc",
+ "react-refresh",
+ "fast refresh"
+ ],
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "tsdown --watch ./src --watch ../common",
+ "build": "tsdown",
+ "test": "playwright test"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vitejs/vite-plugin-react.git",
+ "directory": "packages/plugin-react-swc"
+ },
+ "bugs": {
+ "url": "https://github.com/vitejs/vite-plugin-react/issues"
+ },
+ "homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc#readme",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.41",
+ "@swc/core": "^1.13.5"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.55.1",
+ "@types/fs-extra": "^11.0.4",
+ "@types/node": "^22.18.8",
+ "@vitejs/react-common": "workspace:*",
+ "fs-extra": "^11.3.2",
+ "prettier": "^3.0.3",
+ "tsdown": "^0.15.6",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/base-path/__tests__/base-path.spec.ts b/packages/plugin-react-swc/playground/base-path/__tests__/base-path.spec.ts
new file mode 100644
index 000000000..715eb7a7c
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/__tests__/base-path.spec.ts
@@ -0,0 +1,18 @@
+import { expect, test } from '@playwright/test'
+import { setupDevServer, setupWaitForLogs } from '../../utils.ts'
+
+test('Base path HMR', async ({ page }) => {
+ const { testUrl, server, editFile } = await setupDevServer('base-path')
+ const waitForLogs = await setupWaitForLogs(page)
+ await page.goto(testUrl)
+
+ const button = page.locator('button')
+ await button.click()
+ await expect(button).toHaveText('count is 1')
+
+ editFile('src/App.tsx', ['{count}', '{count}!'])
+ await waitForLogs('[vite] hot updated: /src/App.tsx')
+ await expect(button).toHaveText('count is 1!')
+
+ await server.close()
+})
diff --git a/packages/plugin-react-swc/playground/base-path/index.html b/packages/plugin-react-swc/playground/base-path/index.html
new file mode 100644
index 000000000..75fabbb1e
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS + base path
+
+
+
+
+
+
diff --git a/packages/plugin-react-swc/playground/base-path/package.json b/packages/plugin-react-swc/playground/base-path/package.json
new file mode 100644
index 000000000..ee1e7c8c5
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "playground-base-test",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react-swc": "../../dist"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/base-path/public/vite.svg b/packages/plugin-react-swc/playground/base-path/public/vite.svg
new file mode 100644
index 000000000..4dcd77ad0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-react-swc/playground/base-path/src/App.tsx b/packages/plugin-react-swc/playground/base-path/src/App.tsx
new file mode 100644
index 000000000..d4a5f8f75
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/src/App.tsx
@@ -0,0 +1,7 @@
+import { useState } from 'react'
+
+export const App = () => {
+ const [count, setCount] = useState(0)
+
+ return
+}
diff --git a/packages/plugin-react-swc/playground/base-path/src/index.tsx b/packages/plugin-react-swc/playground/base-path/src/index.tsx
new file mode 100644
index 000000000..5e7046ce2
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/src/index.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { App } from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/packages/plugin-react-swc/playground/base-path/tsconfig.json b/packages/plugin-react-swc/playground/base-path/tsconfig.json
new file mode 100644
index 000000000..6a2d67033
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["src", "vite.config.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true
+ }
+}
diff --git a/packages/plugin-react-swc/playground/base-path/vite.config.ts b/packages/plugin-react-swc/playground/base-path/vite.config.ts
new file mode 100644
index 000000000..7bc60be8f
--- /dev/null
+++ b/packages/plugin-react-swc/playground/base-path/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react()],
+ base: '/base-test/',
+})
diff --git a/packages/plugin-react-swc/playground/class-components/__tests__/class-components.spec.ts b/packages/plugin-react-swc/playground/class-components/__tests__/class-components.spec.ts
new file mode 100644
index 000000000..14424e927
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/__tests__/class-components.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test } from '@playwright/test'
+import { setupDevServer, setupWaitForLogs } from '../../utils.ts'
+
+test('Class component HMR', async ({ page }) => {
+ const { testUrl, server, editFile } = await setupDevServer('class-components')
+ const waitForLogs = await setupWaitForLogs(page)
+ await page.goto(testUrl)
+
+ await expect(page.locator('body')).toHaveText('Hello World')
+ editFile('src/App.tsx', ['World', 'class components'])
+ await waitForLogs('[vite] hot updated: /src/App.tsx')
+ await expect(page.locator('body')).toHaveText('Hello class components')
+
+ editFile('src/utils.tsx', ['Hello', 'Hi'])
+ await waitForLogs('[vite] hot updated: /src/App.tsx')
+ await expect(page.locator('body')).toHaveText('Hi class components')
+
+ await server.close()
+})
diff --git a/packages/plugin-react-swc/playground/class-components/index.html b/packages/plugin-react-swc/playground/class-components/index.html
new file mode 100644
index 000000000..f8e6bda99
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + class components
+
+
+
+
+
+
diff --git a/packages/plugin-react-swc/playground/class-components/package.json b/packages/plugin-react-swc/playground/class-components/package.json
new file mode 100644
index 000000000..0104afa81
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "class-components",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react-swc": "../../dist"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/class-components/public/vite.svg b/packages/plugin-react-swc/playground/class-components/public/vite.svg
new file mode 100644
index 000000000..4dcd77ad0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-react-swc/playground/class-components/src/App.tsx b/packages/plugin-react-swc/playground/class-components/src/App.tsx
new file mode 100644
index 000000000..2b2cb5558
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/src/App.tsx
@@ -0,0 +1,8 @@
+import { Component } from 'react'
+import { getGetting } from './utils.tsx'
+
+export class App extends Component {
+ render() {
+ return {getGetting()} World
+ }
+}
diff --git a/packages/plugin-react-swc/playground/class-components/src/index.tsx b/packages/plugin-react-swc/playground/class-components/src/index.tsx
new file mode 100644
index 000000000..5e7046ce2
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/src/index.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { App } from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/packages/plugin-react-swc/playground/class-components/src/utils.tsx b/packages/plugin-react-swc/playground/class-components/src/utils.tsx
new file mode 100644
index 000000000..27e61766a
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/src/utils.tsx
@@ -0,0 +1 @@
+export const getGetting = () => Hello
diff --git a/packages/plugin-react-swc/playground/class-components/tsconfig.json b/packages/plugin-react-swc/playground/class-components/tsconfig.json
new file mode 100644
index 000000000..6a2d67033
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["src", "vite.config.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true
+ }
+}
diff --git a/packages/plugin-react-swc/playground/class-components/vite.config.ts b/packages/plugin-react-swc/playground/class-components/vite.config.ts
new file mode 100644
index 000000000..7af9f6f97
--- /dev/null
+++ b/packages/plugin-react-swc/playground/class-components/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/packages/plugin-react-swc/playground/decorators/__tests__/decorators.spec.ts b/packages/plugin-react-swc/playground/decorators/__tests__/decorators.spec.ts
new file mode 100644
index 000000000..89783fc2b
--- /dev/null
+++ b/packages/plugin-react-swc/playground/decorators/__tests__/decorators.spec.ts
@@ -0,0 +1,20 @@
+import { expect, test } from '@playwright/test'
+import { setupBuildAndPreview, setupDevServer } from '../../utils.ts'
+
+test('Decorators build', async ({ page }) => {
+ const { testUrl, server } = await setupBuildAndPreview('decorators')
+ await page.goto(testUrl)
+
+ await expect(page.locator('body')).toHaveText('Hello World')
+
+ await server.httpServer.close()
+})
+
+test('Decorators dev', async ({ page }) => {
+ const { testUrl, server } = await setupDevServer('decorators')
+ await page.goto(testUrl)
+
+ await expect(page.locator('body')).toHaveText('Hello World')
+
+ await server.close()
+})
diff --git a/packages/plugin-react-swc/playground/decorators/index.html b/packages/plugin-react-swc/playground/decorators/index.html
new file mode 100644
index 000000000..cb20e5147
--- /dev/null
+++ b/packages/plugin-react-swc/playground/decorators/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS + decorators
+
+
+
+
+
+
diff --git a/packages/plugin-react-swc/playground/decorators/package.json b/packages/plugin-react-swc/playground/decorators/package.json
new file mode 100644
index 000000000..a126f1590
--- /dev/null
+++ b/packages/plugin-react-swc/playground/decorators/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "playground-decorators",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react-swc": "../../dist"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/decorators/public/vite.svg b/packages/plugin-react-swc/playground/decorators/public/vite.svg
new file mode 100644
index 000000000..4dcd77ad0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/decorators/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-react-swc/playground/decorators/src/App.tsx b/packages/plugin-react-swc/playground/decorators/src/App.tsx
new file mode 100644
index 000000000..f8327828c
--- /dev/null
+++ b/packages/plugin-react-swc/playground/decorators/src/App.tsx
@@ -0,0 +1,17 @@
+import type { ComponentClass } from 'react'
+import { Component } from 'react'
+
+function decorated(target: ComponentClass) {
+ const original = target.prototype.render
+
+ target.prototype.render = () => {
+ return
+ Click on the Vite and Styled Components logos to learn more
+
+
+)
diff --git a/packages/plugin-react-swc/playground/styled-components/src/Button.tsx b/packages/plugin-react-swc/playground/styled-components/src/Button.tsx
new file mode 100644
index 000000000..44be3f5bf
--- /dev/null
+++ b/packages/plugin-react-swc/playground/styled-components/src/Button.tsx
@@ -0,0 +1,31 @@
+import styled, { css } from 'styled-components'
+import { useState } from 'react'
+
+// Ensure HMR of styled component alongside other components
+export const StyledCode = styled.code`
+ color: palevioletred;
+`
+
+const Button = styled.button`
+ border-radius: 3px;
+ padding: 0.5rem 1rem;
+ color: white;
+ background: transparent;
+ border: 2px solid white;
+ ${(props: { primary?: boolean }) =>
+ props.primary &&
+ css`
+ background: white;
+ color: black;
+ `}
+`
+
+export const Counter = ({ primary }: { primary?: boolean }) => {
+ const [count, setCount] = useState(0)
+
+ return (
+
+ )
+}
diff --git a/packages/plugin-react-swc/playground/styled-components/src/index.css b/packages/plugin-react-swc/playground/styled-components/src/index.css
new file mode 100644
index 000000000..8d79b2457
--- /dev/null
+++ b/packages/plugin-react-swc/playground/styled-components/src/index.css
@@ -0,0 +1,24 @@
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
diff --git a/packages/plugin-react-swc/playground/styled-components/src/index.tsx b/packages/plugin-react-swc/playground/styled-components/src/index.tsx
new file mode 100644
index 000000000..ee852372c
--- /dev/null
+++ b/packages/plugin-react-swc/playground/styled-components/src/index.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { App } from './App.tsx'
+import './index.css'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/packages/plugin-react-swc/playground/styled-components/tsconfig.json b/packages/plugin-react-swc/playground/styled-components/tsconfig.json
new file mode 100644
index 000000000..6a2d67033
--- /dev/null
+++ b/packages/plugin-react-swc/playground/styled-components/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["src", "vite.config.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true
+ }
+}
diff --git a/packages/plugin-react-swc/playground/styled-components/vite.config.ts b/packages/plugin-react-swc/playground/styled-components/vite.config.ts
new file mode 100644
index 000000000..f852b23ee
--- /dev/null
+++ b/packages/plugin-react-swc/playground/styled-components/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react({ plugins: [['@swc/plugin-styled-components', {}]] })],
+})
diff --git a/packages/plugin-react-swc/playground/ts-lib/__tests__/ts-lib.spec.ts b/packages/plugin-react-swc/playground/ts-lib/__tests__/ts-lib.spec.ts
new file mode 100644
index 000000000..7f3357908
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/__tests__/ts-lib.spec.ts
@@ -0,0 +1,22 @@
+import { type Page, expect, test } from '@playwright/test'
+import { setupBuildAndPreview, setupDevServer } from '../../utils.ts'
+
+test('TS lib build', async ({ page }) => {
+ const { testUrl, server } = await setupBuildAndPreview('ts-lib')
+ await page.goto(testUrl)
+ await testNonJs(page)
+ await server.httpServer.close()
+})
+
+test('TS lib dev', async ({ page }) => {
+ const { testUrl, server } = await setupDevServer('ts-lib')
+ await page.goto(testUrl)
+ await testNonJs(page)
+ await server.close()
+})
+
+async function testNonJs(page: Page) {
+ await expect(page.getByTestId('test-non-js')).toHaveText('test-non-js: 0')
+ await page.getByTestId('test-non-js').click()
+ await expect(page.getByTestId('test-non-js')).toHaveText('test-non-js: 1')
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/index.html b/packages/plugin-react-swc/playground/ts-lib/index.html
new file mode 100644
index 000000000..47589b590
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS lib
+
+
+
+
+
+
diff --git a/packages/plugin-react-swc/playground/ts-lib/package.json b/packages/plugin-react-swc/playground/ts-lib/package.json
new file mode 100644
index 000000000..64a3fcb62
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "playground-ts-lib",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-dep-non-js": "file:./test-dep/non-js",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react-swc": "../../dist"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/public/vite.svg b/packages/plugin-react-swc/playground/ts-lib/public/vite.svg
new file mode 100644
index 000000000..4dcd77ad0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-react-swc/playground/ts-lib/src/app.tsx b/packages/plugin-react-swc/playground/ts-lib/src/app.tsx
new file mode 100644
index 000000000..fc8ba7dfa
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/src/app.tsx
@@ -0,0 +1,11 @@
+import TestNonJs from '@vitejs/test-dep-non-js'
+
+export default function App() {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/src/index.tsx b/packages/plugin-react-swc/playground/ts-lib/src/index.tsx
new file mode 100644
index 000000000..c0e262737
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/src/index.tsx
@@ -0,0 +1,9 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import App from './app'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/index.tsx b/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/index.tsx
new file mode 100644
index 000000000..c3603614c
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/index.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+
+export default function TestNonJs() {
+ const [count, setCount] = React.useState(0)
+ return (
+
+ )
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/package.json b/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/package.json
new file mode 100644
index 000000000..6e098895e
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/test-dep/non-js/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-non-js",
+ "private": true,
+ "type": "module",
+ "exports": "./index.tsx",
+ "peerDependencies": {
+ "react": "*"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/tsconfig.json b/packages/plugin-react-swc/playground/ts-lib/tsconfig.json
new file mode 100644
index 000000000..12ab2344b
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "include": ["src", "vite.config.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "noUncheckedIndexedAccess": true,
+ "noPropertyAccessFromIndexSignature": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true
+ }
+}
diff --git a/packages/plugin-react-swc/playground/ts-lib/vite.config.ts b/packages/plugin-react-swc/playground/ts-lib/vite.config.ts
new file mode 100644
index 000000000..7af9f6f97
--- /dev/null
+++ b/packages/plugin-react-swc/playground/ts-lib/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/packages/plugin-react-swc/playground/utils.ts b/packages/plugin-react-swc/playground/utils.ts
new file mode 100644
index 000000000..751b22f79
--- /dev/null
+++ b/packages/plugin-react-swc/playground/utils.ts
@@ -0,0 +1,120 @@
+import { readFileSync, writeFileSync } from 'node:fs'
+import { type Locator, type Page, expect } from '@playwright/test'
+import {
+ build,
+ createServer,
+ loadConfigFromFile,
+ mergeConfig,
+ preview,
+} from 'vite'
+
+export const setupWaitForLogs = async (page: Page) => {
+ let logs: string[] = []
+ page.on('console', (log) => {
+ logs.push(log.text())
+ })
+ return (...messages: (string | RegExp)[]) =>
+ expect
+ .poll(() => {
+ if (
+ messages.every((m) =>
+ typeof m === 'string'
+ ? logs.includes(m)
+ : logs.some((l) => m.test(l)),
+ )
+ ) {
+ logs = []
+ return true
+ }
+ return logs
+ })
+ .toBe(true)
+}
+
+let port = 5173
+export const setupDevServer = async (name: string) => {
+ process.env['NODE_ENV'] = 'development'
+ const root = `playground-temp/${name}`
+ const res = await loadConfigFromFile(
+ { command: 'serve', mode: 'development' },
+ undefined,
+ root,
+ )
+ const testConfig = mergeConfig(res!.config, {
+ root,
+ logLevel: 'silent',
+ configFile: false,
+ server: { port: port++ },
+ })
+ const server = await (await createServer(testConfig)).listen()
+ return {
+ testUrl: `http://localhost:${server.config.server.port}${server.config.base}`,
+ server,
+ editFile: (
+ name: string,
+ ...replacements: [searchValue: string, replaceValue: string][]
+ ) => {
+ const path = `${root}/${name}`
+ let content = readFileSync(path, 'utf-8')
+ for (const [search, replace] of replacements) {
+ if (!content.includes(search)) {
+ throw new Error(`'${search}' not found in ${name}`)
+ }
+ content = content.replace(search, replace)
+ }
+ writeFileSync(path, content)
+ },
+ }
+}
+
+export const setupBuildAndPreview = async (name: string) => {
+ process.env['NODE_ENV'] = 'production'
+ const root = `playground-temp/${name}`
+ const res = await loadConfigFromFile(
+ { command: 'build', mode: 'production' },
+ undefined,
+ root,
+ )
+ const testConfig = mergeConfig(
+ { root, logLevel: 'silent', configFile: false, preview: { port: port++ } },
+ res!.config,
+ )
+ await build(testConfig)
+ const server = await preview(testConfig)
+ return {
+ testUrl: server.resolvedUrls!.local[0],
+ server,
+ }
+}
+
+export const expectColor = async (
+ locator: Locator,
+ property: 'color' | 'backgroundColor',
+ color: string,
+) => {
+ await expect
+ .poll(async () =>
+ rgbToHex(
+ await locator.evaluate(
+ (el, prop) => getComputedStyle(el)[prop],
+ property,
+ ),
+ ),
+ )
+ .toBe(color)
+}
+
+const rgbToHex = (rgb: string): string => {
+ const [_, rs, gs, bs] = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)!
+ return (
+ '#' +
+ componentToHex(parseInt(rs, 10)) +
+ componentToHex(parseInt(gs, 10)) +
+ componentToHex(parseInt(bs, 10))
+ )
+}
+
+const componentToHex = (c: number): string => {
+ const hex = c.toString(16)
+ return hex.length === 1 ? '0' + hex : hex
+}
diff --git a/packages/plugin-react-swc/playground/worker/__tests__/worker.spec.ts b/packages/plugin-react-swc/playground/worker/__tests__/worker.spec.ts
new file mode 100644
index 000000000..b0c7b2ce0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/__tests__/worker.spec.ts
@@ -0,0 +1,27 @@
+import { test } from '@playwright/test'
+import {
+ setupBuildAndPreview,
+ setupDevServer,
+ setupWaitForLogs,
+} from '../../utils.ts'
+
+test('Worker build', async ({ page }) => {
+ const { testUrl, server } = await setupBuildAndPreview('worker')
+ const waitForLogs = await setupWaitForLogs(page)
+ await page.goto(testUrl)
+ await waitForLogs('Worker lives!', 'Worker imported!')
+
+ await server.httpServer.close()
+})
+
+test('Worker HMR', async ({ page }) => {
+ const { testUrl, server, editFile } = await setupDevServer('worker')
+ const waitForLogs = await setupWaitForLogs(page)
+ await page.goto(testUrl)
+ await waitForLogs('Worker lives!', 'Worker imported!')
+
+ editFile('src/worker-via-url.ts', ['Worker lives!', 'Worker updates!'])
+ await waitForLogs('Worker updates!')
+
+ await server.close()
+})
diff --git a/packages/plugin-react-swc/playground/worker/index.html b/packages/plugin-react-swc/playground/worker/index.html
new file mode 100644
index 000000000..5f14b23a4
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + Worker
+
+
+
+
+
+
diff --git a/packages/plugin-react-swc/playground/worker/package.json b/packages/plugin-react-swc/playground/worker/package.json
new file mode 100644
index 000000000..a54a1d437
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "playground-worker",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react-swc": "../../dist"
+ }
+}
diff --git a/packages/plugin-react-swc/playground/worker/public/vite.svg b/packages/plugin-react-swc/playground/worker/public/vite.svg
new file mode 100644
index 000000000..4dcd77ad0
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-react-swc/playground/worker/src/App.tsx b/packages/plugin-react-swc/playground/worker/src/App.tsx
new file mode 100644
index 000000000..055838c28
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/src/App.tsx
@@ -0,0 +1,10 @@
+import { useState } from 'react'
+import MyWorker from './worker-via-import.ts?worker&inline'
+
+new MyWorker()
+
+export const App = () => {
+ const [count, setCount] = useState(0)
+
+ return
+}
diff --git a/packages/plugin-react-swc/playground/worker/src/index.tsx b/packages/plugin-react-swc/playground/worker/src/index.tsx
new file mode 100644
index 000000000..317caffbd
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/src/index.tsx
@@ -0,0 +1,11 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { App } from './App.tsx'
+
+new Worker(new URL('./worker-via-url.ts', import.meta.url), { type: 'module' })
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/packages/plugin-react-swc/playground/worker/src/worker-via-import.ts b/packages/plugin-react-swc/playground/worker/src/worker-via-import.ts
new file mode 100644
index 000000000..a90f7ac3d
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/src/worker-via-import.ts
@@ -0,0 +1,7 @@
+function printAlive(): void {
+ console.log('Worker imported!')
+}
+
+printAlive()
+
+export {}
diff --git a/packages/plugin-react-swc/playground/worker/src/worker-via-url.ts b/packages/plugin-react-swc/playground/worker/src/worker-via-url.ts
new file mode 100644
index 000000000..06a383a2a
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/src/worker-via-url.ts
@@ -0,0 +1,7 @@
+function printAlive(): void {
+ console.log('Worker lives!')
+}
+
+printAlive()
+
+export {}
diff --git a/packages/plugin-react-swc/playground/worker/tsconfig.json b/packages/plugin-react-swc/playground/worker/tsconfig.json
new file mode 100644
index 000000000..6a2d67033
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["src", "vite.config.ts"],
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+ "noEmit": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true
+ }
+}
diff --git a/packages/plugin-react-swc/playground/worker/vite.config.ts b/packages/plugin-react-swc/playground/worker/vite.config.ts
new file mode 100644
index 000000000..7af9f6f97
--- /dev/null
+++ b/packages/plugin-react-swc/playground/worker/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/packages/plugin-react-swc/playwright.config.ts b/packages/plugin-react-swc/playwright.config.ts
new file mode 100644
index 000000000..d7c8ae487
--- /dev/null
+++ b/packages/plugin-react-swc/playwright.config.ts
@@ -0,0 +1,27 @@
+import { fileURLToPath } from 'node:url'
+import { type PlaywrightTestConfig, devices } from '@playwright/test'
+import fs from 'fs-extra'
+
+const tempDir = fileURLToPath(new URL('playground-temp', import.meta.url))
+fs.ensureDirSync(tempDir)
+fs.emptyDirSync(tempDir)
+fs.copySync(fileURLToPath(new URL('playground', import.meta.url)), tempDir, {
+ filter: (src) => {
+ src = src.replaceAll('\\', '/')
+ return (
+ !src.includes('/__tests__') &&
+ !src.includes('/.vite') &&
+ !src.includes('/dist')
+ )
+ },
+})
+
+const config: PlaywrightTestConfig = {
+ forbidOnly: !!process.env['CI'],
+ workers: 1,
+ timeout: 10_000,
+ reporter: process.env['CI'] ? 'github' : 'list',
+ projects: [{ name: 'chromium', use: devices['Desktop Chrome'] }],
+}
+
+export default config
diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts
new file mode 100644
index 000000000..425bac15c
--- /dev/null
+++ b/packages/plugin-react-swc/src/index.ts
@@ -0,0 +1,324 @@
+import { readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { createRequire } from 'node:module'
+import {
+ type JscTarget,
+ type Output,
+ type ParserConfig,
+ type ReactConfig,
+ type Options as SWCOptions,
+ transform,
+} from '@swc/core'
+import type { Plugin } from 'vite'
+import {
+ addRefreshWrapper,
+ getPreambleCode,
+ runtimePublicPath,
+ silenceUseClientWarning,
+} from '@vitejs/react-common'
+import * as vite from 'vite'
+import { exactRegex } from '@rolldown/pluginutils'
+
+/* eslint-disable no-restricted-globals */
+const _dirname =
+ typeof __dirname !== 'undefined'
+ ? __dirname
+ : dirname(fileURLToPath(import.meta.url))
+const resolve = createRequire(
+ typeof __filename !== 'undefined' ? __filename : import.meta.url,
+).resolve
+/* eslint-enable no-restricted-globals */
+
+type Options = {
+ /**
+ * Control where the JSX factory is imported from.
+ * @default "react"
+ */
+ jsxImportSource?: string
+ /**
+ * Enable TypeScript decorators. Requires experimentalDecorators in tsconfig.
+ * @default false
+ */
+ tsDecorators?: boolean
+ /**
+ * Use SWC plugins. Enable SWC at build time.
+ * @default undefined
+ */
+ plugins?: [string, Record][]
+ /**
+ * Set the target for SWC in dev. This can avoid to down-transpile private class method for example.
+ * For production target, see https://vite.dev/config/build-options.html#build-target
+ * @default "es2020"
+ */
+ devTarget?: JscTarget
+ /**
+ * Override the default include list (.ts, .tsx, .mts, .jsx, .mdx).
+ * This requires to redefine the config for any file you want to be included.
+ * If you want to trigger fast refresh on compiled JS, use `jsx: true`.
+ * Exclusion of node_modules should be handled by the function if needed.
+ */
+ parserConfig?: (id: string) => ParserConfig | undefined
+ /**
+ * React Fast Refresh runtime URL prefix.
+ * Useful in a module federation context to enable HMR by specifying
+ * the host application URL in a Vite config of a remote application.
+ * @example
+ * reactRefreshHost: 'http://localhost:3000'
+ */
+ reactRefreshHost?: string
+ /**
+ * 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.
+ * 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`
+ */
+ useAtYourOwnRisk_mutateSwcOptions?: (options: SWCOptions) => void
+
+ /**
+ * If set, disables the recommendation to use `@vitejs/plugin-react`
+ */
+ disableOxcRecommendation?: boolean
+}
+
+const react = (_options?: Options): Plugin[] => {
+ let hmrDisabled = false
+ let viteCacheRoot: string | undefined
+ const options = {
+ jsxImportSource: _options?.jsxImportSource ?? 'react',
+ tsDecorators: _options?.tsDecorators,
+ plugins: _options?.plugins
+ ? _options?.plugins.map((el): typeof el => [resolve(el[0]), el[1]])
+ : undefined,
+ devTarget: _options?.devTarget ?? 'es2020',
+ parserConfig: _options?.parserConfig,
+ reactRefreshHost: _options?.reactRefreshHost,
+ useAtYourOwnRisk_mutateSwcOptions:
+ _options?.useAtYourOwnRisk_mutateSwcOptions,
+ disableOxcRecommendation: _options?.disableOxcRecommendation,
+ }
+
+ return [
+ {
+ name: 'vite:react-swc:resolve-runtime',
+ apply: 'serve',
+ enforce: 'pre', // Run before Vite default resolve to avoid syscalls
+ resolveId: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler: (id) => (id === runtimePublicPath ? id : undefined),
+ },
+ load: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler: (id) =>
+ id === runtimePublicPath
+ ? readFileSync(
+ join(_dirname, 'refresh-runtime.js'),
+ 'utf-8',
+ ).replace(
+ /__README_URL__/g,
+ 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc',
+ )
+ : undefined,
+ },
+ },
+ {
+ name: 'vite:react-swc',
+ apply: 'serve',
+ config: () => ({
+ esbuild: false,
+ // NOTE: oxc option only exists in rolldown-vite
+ oxc: false,
+ optimizeDeps: {
+ include: [`${options.jsxImportSource}/jsx-dev-runtime`],
+ ...('rolldownVersion' in vite
+ ? {
+ rollupOptions: { transform: { jsx: { runtime: 'automatic' } } },
+ }
+ : { esbuildOptions: { jsx: 'automatic' } }),
+ },
+ }),
+ configResolved(config) {
+ viteCacheRoot = config.cacheDir
+ if (config.server.hmr === false) hmrDisabled = true
+ const mdxIndex = config.plugins.findIndex(
+ (p) => p.name === '@mdx-js/rollup',
+ )
+ if (
+ mdxIndex !== -1 &&
+ mdxIndex >
+ config.plugins.findIndex((p) => p.name === 'vite:react-swc')
+ ) {
+ throw new Error(
+ '[vite:react-swc] The MDX plugin should be placed before this plugin',
+ )
+ }
+
+ if (
+ 'rolldownVersion' in vite &&
+ !options.plugins &&
+ !options.useAtYourOwnRisk_mutateSwcOptions &&
+ !options.disableOxcRecommendation
+ ) {
+ config.logger.warn(
+ '[vite:react-swc] We recommend switching to `@vitejs/plugin-react` for improved performance as no swc plugins are used. More information at https://vite.dev/rolldown',
+ )
+ }
+ },
+ transformIndexHtml: (_, config) => {
+ if (!hmrDisabled) {
+ return [
+ {
+ tag: 'script',
+ attrs: { type: 'module' },
+ children: getPreambleCode(config.server!.config.base),
+ },
+ ]
+ }
+ },
+ async transform(code, _id, transformOptions) {
+ const id = _id.split('?')[0]
+ const refresh = !transformOptions?.ssr && !hmrDisabled
+
+ const result = await transformWithOptions(
+ id,
+ code,
+ options.devTarget,
+ options,
+ viteCacheRoot,
+ {
+ refresh,
+ development: true,
+ runtime: 'automatic',
+ importSource: options.jsxImportSource,
+ },
+ )
+ if (!result) return
+ if (!refresh) return result
+
+ const newCode = addRefreshWrapper(
+ result.code,
+ '@vitejs/plugin-react-swc',
+ id,
+ options.reactRefreshHost,
+ )
+ return { code: newCode ?? result.code, map: result.map }
+ },
+ },
+ options.plugins
+ ? {
+ name: 'vite:react-swc',
+ apply: 'build',
+ enforce: 'pre', // Run before esbuild
+ config: (userConfig) => ({
+ build: silenceUseClientWarning(userConfig),
+ }),
+ configResolved(config) {
+ viteCacheRoot = config.cacheDir
+ },
+ transform: (code, _id) =>
+ transformWithOptions(
+ _id.split('?')[0],
+ code,
+ 'esnext',
+ options,
+ viteCacheRoot,
+ {
+ runtime: 'automatic',
+ importSource: options.jsxImportSource,
+ },
+ ),
+ }
+ : {
+ name: 'vite:react-swc',
+ apply: 'build',
+ config: (userConfig) => ({
+ build: silenceUseClientWarning(userConfig),
+ esbuild: {
+ jsx: 'automatic',
+ jsxImportSource: options.jsxImportSource,
+ tsconfigRaw: {
+ compilerOptions: { useDefineForClassFields: true },
+ },
+ },
+ }),
+ configResolved(config) {
+ viteCacheRoot = config.cacheDir
+ },
+ },
+ ]
+}
+
+const transformWithOptions = async (
+ id: string,
+ code: string,
+ target: JscTarget,
+ options: Options,
+ viteCacheRoot: string | undefined,
+ reactConfig: ReactConfig,
+) => {
+ const decorators = options?.tsDecorators ?? false
+ const parser: ParserConfig | undefined = options.parserConfig
+ ? options.parserConfig(id)
+ : id.endsWith('.tsx')
+ ? { syntax: 'typescript', tsx: true, decorators }
+ : id.endsWith('.ts') || id.endsWith('.mts')
+ ? { syntax: 'typescript', tsx: false, decorators }
+ : id.endsWith('.jsx')
+ ? { syntax: 'ecmascript', jsx: true }
+ : id.endsWith('.mdx')
+ ? // JSX is required to trigger fast refresh transformations, even if MDX already transforms it
+ { syntax: 'ecmascript', jsx: true }
+ : undefined
+ if (!parser) return
+
+ let result: Output
+ try {
+ const swcOptions: SWCOptions = {
+ filename: id,
+ swcrc: false,
+ configFile: false,
+ sourceMaps: true,
+ jsc: {
+ target,
+ parser,
+ experimental: {
+ plugins: options.plugins,
+ cacheRoot: join(viteCacheRoot ?? 'node_modules/.vite', '.swc'),
+ },
+ transform: {
+ useDefineForClassFields: true,
+ react: reactConfig,
+ },
+ },
+ }
+ if (options.useAtYourOwnRisk_mutateSwcOptions) {
+ options.useAtYourOwnRisk_mutateSwcOptions(swcOptions)
+ }
+ result = await transform(code, swcOptions)
+ } catch (e: any) {
+ const message: string = e.message
+ const fileStartIndex = message.indexOf('╭─[')
+ if (fileStartIndex !== -1) {
+ const match = message.slice(fileStartIndex).match(/:(\d+):(\d+)\]/)
+ if (match) {
+ e.line = match[1]
+ e.column = match[2]
+ }
+ }
+ throw e
+ }
+
+ return result
+}
+
+export default react
+
+// Compat for require
+function pluginForCjs(this: unknown, options: Options): Plugin[] {
+ return react.call(this, options)
+}
+Object.assign(pluginForCjs, {
+ default: pluginForCjs,
+})
+export { pluginForCjs as 'module.exports' }
diff --git a/packages/plugin-react-swc/tsconfig.json b/packages/plugin-react-swc/tsconfig.json
new file mode 100644
index 000000000..ac687cd46
--- /dev/null
+++ b/packages/plugin-react-swc/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "include": [],
+ "references": [
+ { "path": "./tsconfig.src.json" },
+ { "path": "./tsconfig.test.json" }
+ ]
+}
diff --git a/packages/plugin-react-swc/tsconfig.src.json b/packages/plugin-react-swc/tsconfig.src.json
new file mode 100644
index 000000000..4194367e6
--- /dev/null
+++ b/packages/plugin-react-swc/tsconfig.src.json
@@ -0,0 +1,27 @@
+{
+ "include": ["src"],
+ "compilerOptions": {
+ /* Target node 22 */
+ "module": "ESNext",
+ "lib": ["ES2023", "DOM"],
+ "target": "ES2023",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "declaration": true,
+ "isolatedDeclarations": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true,
+ "noUncheckedSideEffectImports": true,
+ "noPropertyAccessFromIndexSignature": true
+ }
+}
diff --git a/packages/plugin-react-swc/tsconfig.test.json b/packages/plugin-react-swc/tsconfig.test.json
new file mode 100644
index 000000000..8bc9cb430
--- /dev/null
+++ b/packages/plugin-react-swc/tsconfig.test.json
@@ -0,0 +1,25 @@
+{
+ "include": ["playwright.config.ts", "playground"],
+ "compilerOptions": {
+ /* Target node 22 */
+ "module": "ESNext",
+ "lib": ["ES2023", "DOM"],
+ "target": "ES2023",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "useUnknownInCatchVariables": true,
+ "noUncheckedSideEffectImports": true,
+ "noPropertyAccessFromIndexSignature": true
+ }
+}
diff --git a/packages/plugin-react-swc/tsdown.config.ts b/packages/plugin-react-swc/tsdown.config.ts
new file mode 100644
index 000000000..354d5f2e6
--- /dev/null
+++ b/packages/plugin-react-swc/tsdown.config.ts
@@ -0,0 +1,44 @@
+import { writeFileSync } from 'node:fs'
+import { defineConfig } from 'tsdown'
+import packageJSON from './package.json' with { type: 'json' }
+
+export default defineConfig({
+ entry: 'src/index.ts',
+ dts: true,
+ tsconfig: './tsconfig.src.json', // https://github.com/sxzz/rolldown-plugin-dts/issues/55
+ ignoreWatch: ['playground', 'playground-temp', 'test-results'],
+ copy: [
+ {
+ from: 'node_modules/@vitejs/react-common/refresh-runtime.js',
+ to: 'dist/refresh-runtime.js',
+ },
+ {
+ from: 'LICENSE',
+ to: 'dist/LICENSE',
+ },
+ {
+ from: 'README.md',
+ to: 'dist/README.md',
+ },
+ ],
+ onSuccess() {
+ writeFileSync(
+ 'dist/package.json',
+ JSON.stringify(
+ {
+ ...Object.fromEntries(
+ Object.entries(packageJSON).filter(
+ ([key, _val]) =>
+ key !== 'devDependencies' &&
+ key !== 'scripts' &&
+ key !== 'private',
+ ),
+ ),
+ exports: './index.js',
+ },
+ null,
+ 2,
+ ),
+ )
+ },
+})
diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md
index 969cbb31d..6ef1b7750 100644
--- a/packages/plugin-react/CHANGELOG.md
+++ b/packages/plugin-react/CHANGELOG.md
@@ -2,6 +2,197 @@
## Unreleased
+## 5.0.4 (2025-09-27)
+
+### Perf: use native refresh wrapper plugin in rolldown-vite ([#881](https://github.com/vitejs/vite-plugin-react/pull/881))
+
+## 5.0.3 (2025-09-17)
+
+### HMR did not work for components imported with queries with rolldown-vite ([#872](https://github.com/vitejs/vite-plugin-react/pull/872))
+
+### Perf: simplify refresh wrapper generation ([#835](https://github.com/vitejs/vite-plugin-react/pull/835))
+
+## 5.0.2 (2025-08-28)
+
+### Skip transform hook completely in rolldown-vite in dev if possible ([#783](https://github.com/vitejs/vite-plugin-react/pull/783))
+
+## 5.0.1 (2025-08-19)
+
+### Set `optimizeDeps.rollupOptions.transform.jsx` instead of `optimizeDeps.rollupOptions.jsx` for rolldown-vite ([#735](https://github.com/vitejs/vite-plugin-react/pull/735))
+
+`optimizeDeps.rollupOptions.jsx` is going to be deprecated in favor of `optimizeDeps.rollupOptions.transform.jsx`.
+
+### Perf: skip `babel-plugin-react-compiler` if code has no `"use memo"` when `{ compilationMode: "annotation" }` ([#734](https://github.com/vitejs/vite-plugin-react/pull/734))
+
+### Respect tsconfig `jsxImportSource` ([#726](https://github.com/vitejs/vite-plugin-react/pull/726))
+
+### Fix `reactRefreshHost` option on rolldown-vite ([#716](https://github.com/vitejs/vite-plugin-react/pull/716))
+
+### Fix `RefreshRuntime` being injected twice for class components on rolldown-vite ([#708](https://github.com/vitejs/vite-plugin-react/pull/708))
+
+### Skip `babel-plugin-react-compiler` on non client environment ([689](https://github.com/vitejs/vite-plugin-react/pull/689))
+
+## 5.0.0 (2025-08-07)
+
+## 5.0.0-beta.0 (2025-07-28)
+
+### Use Oxc for react refresh transform in rolldown-vite
+
+When used with rolldown-vite, this plugin now uses Oxc for react refresh transform.
+
+Since this behavior is what `@vitejs/plugin-react-oxc` did, `@vitejs/plugin-react-oxc` is now deprecated and the `disableOxcRecommendation` option is removed.
+
+Also, while `@vitejs/plugin-react-oxc` used the production JSX transform even for `NODE_ENV=development` build, `@vitejs/plugin-react` uses the development JSX transform for `NODE_ENV=development` build.
+
+### Allow processing files in `node_modules`
+
+The default value of `exclude` options is now `[/\/node_modules\//]` to allow processing files in `node_modules` directory. It was previously `[]` and files in `node_modules` was always excluded regardless of the value of `exclude` option.
+
+### `react` and `react-dom` is no longer added to [`resolve.dedupe`](https://vite.dev/config/#resolve-dedupe) automatically
+
+Adding values to `resolve.dedupe` forces Vite to resolve them differently from how Node.js does, which can be confusing and may not be expected. This plugin no longer adds `react` and `react-dom` to `resolve.dedupe` automatically.
+
+If you encounter errors after upgrading, check your package.json for version mismatches in `dependencies` or `devDependencies`, as well as your package manager’s configuration. If you prefer the previous behavior, you can manually add `react` and `react-dom` to `resolve.dedupe`.
+
+### Remove old `babel-plugin-react-compiler` support that requires `runtimeModule` option
+
+`runtimeModule` option is no longer needed in newer `babel-plugin-react-compiler` versions. Make sure to use a newer version of `babel-plugin-react-compiler` that supports `target` option.
+
+### Require Node 20.19+, 22.12+
+
+This plugin now requires Node 20.19+ or 22.12+.
+
+## 4.7.0 (2025-07-18)
+
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () =>
Accordion Root
+const Item = () =>
Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
+### Return `Plugin[]` instead of `PluginOption[]` ([#537](https://github.com/vitejs/vite-plugin-react/pull/537))
+
+The return type has changed from `react(): PluginOption[]` to more specialized type `react(): Plugin[]`. This allows for type-safe manipulation of plugins, for example:
+
+```tsx
+// previously this causes type errors
+react({ babel: { plugins: ['babel-plugin-react-compiler'] } })
+ .map(p => ({ ...p, applyToEnvironment: e => e.name === 'client' }))
+```
+
+## 4.6.0 (2025-06-23)
+
+### Add raw Rolldown support
+
+This plugin only worked with Vite. But now it can also be used with raw Rolldown. The main purpose for using this plugin with Rolldown is to use react compiler.
+
+## 4.5.2 (2025-06-10)
+
+### Suggest `@vitejs/plugin-react-oxc` if rolldown-vite is detected [#491](https://github.com/vitejs/vite-plugin-react/pull/491)
+
+Emit a log which recommends `@vitejs/plugin-react-oxc` when `rolldown-vite` is detected to improve performance and use Oxc under the hood. The warning can be disabled by setting `disableOxcRecommendation: true` in the plugin options.
+
+### Use `optimizeDeps.rollupOptions` instead of `optimizeDeps.esbuildOptions` for rolldown-vite [#489](https://github.com/vitejs/vite-plugin-react/pull/489)
+
+This suppresses the warning about `optimizeDeps.esbuildOptions` being deprecated in rolldown-vite.
+
+### Add Vite 7-beta to peerDependencies range [#497](https://github.com/vitejs/vite-plugin-react/pull/497)
+
+React plugins are compatible with Vite 7, this removes the warning when testing the beta.
+
+## 4.5.1 (2025-06-03)
+
+### Add explicit semicolon in preambleCode [#485](https://github.com/vitejs/vite-plugin-react/pull/485)
+
+This fixes an edge case when using HTML minifiers that strips line breaks aggressively.
+
+## 4.5.0 (2025-05-23)
+
+### Add `filter` for rolldown-vite [#470](https://github.com/vitejs/vite-plugin-react/pull/470)
+
+Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite.
+
+### Skip HMR for JSX files with hooks [#480](https://github.com/vitejs/vite-plugin-react/pull/480)
+
+This removes the HMR warning for hooks with JSX.
+
+## 4.4.1 (2025-04-19)
+
+Fix type issue when using `moduleResolution: "node"` in tsconfig [#462](https://github.com/vitejs/vite-plugin-react/pull/462)
+
+## 4.4.0 (2025-04-15)
+
+### Make compatible with rolldown-vite
+
+This plugin is now compatible with rolldown-powered version of Vite.
+Note that currently the `__source` property value position might be incorrect. This will be fixed in the near future.
+
+## 4.4.0-beta.2 (2025-04-15)
+
+### Add `reactRefreshHost` option
+
+Add `reactRefreshHost` option to set a React Fast Refresh runtime URL prefix.
+This is useful in a module federation context to enable HMR by specifying the host application URL in the Vite config of a remote application.
+See full discussion here: https://github.com/module-federation/vite/issues/183#issuecomment-2751825367
+
+```ts
+export default defineConfig({
+ plugins: [react({ reactRefreshHost: 'http://localhost:3000' })],
+})
+```
+
+## 4.4.0-beta.1 (2025-04-09)
+
+## 4.4.0-beta.0 (2025-04-09)
+
+## 4.3.4 (2024-11-26)
+
+### Add Vite 6 to peerDependencies range
+
+Vite 6 is highly backward compatible, not much to add!
+
+### Force Babel to output spec compliant import attributes [#386](https://github.com/vitejs/vite-plugin-react/pull/386)
+
+The default was an old spec (`with type: "json"`). We now enforce spec compliant (`with { type: "json" }`)
+
+## 4.3.3 (2024-10-19)
+
+### React Compiler runtimeModule option removed
+
+React Compiler was updated to accept a `target` option and `runtimeModule` was removed. vite-plugin-react will still detect `runtimeModule` for backwards compatibility.
+
+When using a custom `runtimeModule` or `target !== '19'`, the plugin will not try to pre-optimize `react/compiler-runtime` dependency.
+
+The [react-compiler-runtime](https://www.npmjs.com/package/react-compiler-runtime) is now available on npm can be used instead of the local shim for people using the compiler with React < 19.
+
+Here is the configuration to use the compiler with React 18 and correct source maps in development:
+
+```bash
+npm install babel-plugin-react-compiler react-compiler-runtime @babel/plugin-transform-react-jsx-development
+```
+
+```ts
+export default defineConfig(({ command }) => {
+ const babelPlugins = [['babel-plugin-react-compiler', { target: '18' }]]
+ if (command === 'serve') {
+ babelPlugins.push(['@babel/plugin-transform-react-jsx-development', {}])
+ }
+
+ return {
+ plugins: [react({ babel: { plugins: babelPlugins } })],
+ }
+})
+````
+
+## 4.3.2 (2024-09-29)
+
+Ignore directive sourcemap error [#369](https://github.com/vitejs/vite-plugin-react/issues/369)
+
## 4.3.1 (2024-06-10)
### Fix support for React Compiler with React 18
@@ -424,7 +615,7 @@ See the [readme](https://github.com/aleclarson/vite/blob/f8129ce6e87684eb7a4edd8
- Support for [automatic JSX runtime](https://github.com/alloc/vite-react-jsx)
- Babel integration for both development and production builds
-- Add `react` and `react-dom` to [`resolve.dedupe`](https://vitejs.dev/config/#resolve-dedupe) automatically
+- Add `react` and `react-dom` to [`resolve.dedupe`](https://vite.dev/config/#resolve-dedupe) automatically
Thanks to @aleclarson and @pengx17 for preparing this release!
diff --git a/packages/plugin-react/README.md b/packages/plugin-react/README.md
index 5220bae4d..c607891ca 100644
--- a/packages/plugin-react/README.md
+++ b/packages/plugin-react/README.md
@@ -21,7 +21,7 @@ export default defineConfig({
### include/exclude
-Includes `.js`, `.jsx`, `.ts` & `.tsx` by default. This option can be used to add fast refresh to `.mdx` files:
+Includes `.js`, `.jsx`, `.ts` & `.tsx` and excludes `/node_modules/` by default. This option can be used to add fast refresh to `.mdx` files:
```js
import { defineConfig } from 'vite'
@@ -36,11 +36,9 @@ export default defineConfig({
})
```
-> `node_modules` are never processed by this plugin (but esbuild will)
-
### jsxImportSource
-Control where the JSX factory is imported from. Default to `'react'`
+Control where the JSX factory is imported from. By default, this is inferred from `jsxImportSource` from corresponding a tsconfig file for a transformed file.
```js
react({ jsxImportSource: '@emotion/react' })
@@ -94,9 +92,19 @@ This option does not enable _code transformation_. That is handled by esbuild.
Here's the [complete list of Babel parser plugins](https://babeljs.io/docs/en/babel-parser#ecmascript-proposalshttpsgithubcombabelproposals).
+### reactRefreshHost
+
+The `reactRefreshHost` option is only necessary in a module federation context. It enables HMR to work between a remote & host application. In your remote Vite config, you would add your host origin:
+
+```js
+react({ reactRefreshHost: 'http://localhost:3000' })
+```
+
+Under the hood, this simply updates the React Fash Refresh runtime URL from `/@react-refresh` to `http://localhost:3000/@react-refresh` to ensure there is only one Refresh runtime across the whole application. Note that if you define `base` option in the host application, you need to include it in the option, like: `http://localhost:3000/{base}`.
+
## Middleware mode
-In [middleware mode](https://vitejs.dev/config/server-options.html#server-middlewaremode), you should make sure your entry `index.html` file is transformed by Vite. Here's an example for an Express server:
+In [middleware mode](https://vite.dev/config/server-options.html#server-middlewaremode), you should make sure your entry `index.html` file is transformed by Vite. Here's an example for an Express server:
```js
app.get('/', async (req, res, next) => {
diff --git a/packages/plugin-react/build.config.ts b/packages/plugin-react/build.config.ts
deleted file mode 100644
index 9d05aa4fe..000000000
--- a/packages/plugin-react/build.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineBuildConfig } from 'unbuild'
-
-export default defineBuildConfig({
- entries: ['src/index'],
- externals: ['vite'],
- clean: true,
- declaration: true,
- rollup: {
- emitCJS: true,
- inlineDependencies: true,
- },
-})
diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json
index e4a225264..1417828c5 100644
--- a/packages/plugin-react/package.json
+++ b/packages/plugin-react/package.json
@@ -1,8 +1,17 @@
{
"name": "@vitejs/plugin-react",
- "version": "4.3.1",
+ "version": "5.0.4",
"license": "MIT",
"author": "Evan You",
+ "description": "The default Vite plugin for React projects",
+ "keywords": [
+ "vite",
+ "vite-plugin",
+ "react",
+ "babel",
+ "react-refresh",
+ "fast refresh"
+ ],
"contributors": [
"Alec Larson",
"Arnaud Barré"
@@ -10,23 +19,16 @@
"files": [
"dist"
],
- "main": "./dist/index.cjs",
- "module": "./dist/index.mjs",
- "types": "./dist/index.d.ts",
- "exports": {
- ".": {
- "import": "./dist/index.mjs",
- "require": "./dist/index.cjs"
- }
- },
+ "type": "module",
+ "exports": "./dist/index.js",
"scripts": {
- "dev": "unbuild --stub",
- "build": "unbuild && pnpm run patch-cjs && tsx scripts/copyRefreshUtils.ts",
- "patch-cjs": "tsx ../../scripts/patchCJS.ts",
- "prepublishOnly": "npm run build"
+ "dev": "tsdown --watch ./src --watch ../common",
+ "build": "tsdown",
+ "prepublishOnly": "npm run build",
+ "test-unit": "vitest run"
},
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"repository": {
"type": "git",
@@ -38,16 +40,22 @@
},
"homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme",
"dependencies": {
- "@babel/core": "^7.24.5",
- "@babel/plugin-transform-react-jsx-self": "^7.24.5",
- "@babel/plugin-transform-react-jsx-source": "^7.24.1",
+ "@babel/core": "^7.28.4",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.41",
"@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
+ "react-refresh": "^0.17.0"
},
"peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0"
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
},
"devDependencies": {
- "unbuild": "^2.0.0"
+ "@vitejs/react-common": "workspace:*",
+ "babel-plugin-react-compiler": "19.1.0-rc.3",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "rolldown": "1.0.0-beta.41",
+ "tsdown": "^0.15.6"
}
}
diff --git a/packages/plugin-react/scripts/copyRefreshUtils.ts b/packages/plugin-react/scripts/copyRefreshUtils.ts
deleted file mode 100644
index 977f6442f..000000000
--- a/packages/plugin-react/scripts/copyRefreshUtils.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { copyFileSync } from 'node:fs'
-
-copyFileSync('src/refreshUtils.js', 'dist/refreshUtils.js')
diff --git a/packages/plugin-react/src/fast-refresh.ts b/packages/plugin-react/src/fast-refresh.ts
deleted file mode 100644
index 3f9ffa605..000000000
--- a/packages/plugin-react/src/fast-refresh.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import fs from 'node:fs'
-import path from 'node:path'
-import { createRequire } from 'node:module'
-
-export const runtimePublicPath = '/@react-refresh'
-
-const _require = createRequire(import.meta.url)
-const reactRefreshDir = path.dirname(
- _require.resolve('react-refresh/package.json'),
-)
-const runtimeFilePath = path.join(
- reactRefreshDir,
- 'cjs/react-refresh-runtime.development.js',
-)
-
-export const runtimeCode = `
-const exports = {}
-${fs.readFileSync(runtimeFilePath, 'utf-8')}
-${fs.readFileSync(_require.resolve('./refreshUtils.js'), 'utf-8')}
-export default exports
-`
-
-export const preambleCode = `
-import RefreshRuntime from "__BASE__${runtimePublicPath.slice(1)}"
-RefreshRuntime.injectIntoGlobalHook(window)
-window.$RefreshReg$ = () => {}
-window.$RefreshSig$ = () => (type) => type
-window.__vite_plugin_react_preamble_installed__ = true
-`
-
-const sharedHeader = `
-import RefreshRuntime from "${runtimePublicPath}";
-
-const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
-`.replace(/\n+/g, '')
-const functionHeader = `
-let prevRefreshReg;
-let prevRefreshSig;
-
-if (import.meta.hot && !inWebWorker) {
- if (!window.__vite_plugin_react_preamble_installed__) {
- throw new Error(
- "@vitejs/plugin-react can't detect preamble. Something is wrong. " +
- "See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201"
- );
- }
-
- prevRefreshReg = window.$RefreshReg$;
- prevRefreshSig = window.$RefreshSig$;
- window.$RefreshReg$ = (type, id) => {
- RefreshRuntime.register(type, __SOURCE__ + " " + id)
- };
- window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
-}`.replace(/\n+/g, '')
-
-const functionFooter = `
-if (import.meta.hot && !inWebWorker) {
- window.$RefreshReg$ = prevRefreshReg;
- window.$RefreshSig$ = prevRefreshSig;
-}`
-const sharedFooter = `
-if (import.meta.hot && !inWebWorker) {
- RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
- RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports);
- import.meta.hot.accept((nextExports) => {
- if (!nextExports) return;
- const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports);
- if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
- });
- });
-}`
-
-export function addRefreshWrapper(code: string, id: string): string {
- return (
- sharedHeader +
- functionHeader.replace('__SOURCE__', JSON.stringify(id)) +
- code +
- functionFooter +
- sharedFooter.replace('__SOURCE__', JSON.stringify(id))
- )
-}
-
-export function addClassComponentRefreshWrapper(
- code: string,
- id: string,
-): string {
- return (
- sharedHeader + code + sharedFooter.replace('__SOURCE__', JSON.stringify(id))
- )
-}
diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts
index 9d2356e3f..a01f693e5 100644
--- a/packages/plugin-react/src/index.ts
+++ b/packages/plugin-react/src/index.ts
@@ -1,22 +1,25 @@
-// eslint-disable-next-line import/no-duplicates
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { readFileSync } from 'node:fs'
import type * as babelCore from '@babel/core'
-// eslint-disable-next-line import/no-duplicates
import type { ParserOptions, TransformOptions } from '@babel/core'
import { createFilter } from 'vite'
-import type {
- BuildOptions,
- Plugin,
- PluginOption,
- ResolvedConfig,
- UserConfig,
-} from 'vite'
+import * as vite from 'vite'
+import type { Plugin, ResolvedConfig } from 'vite'
import {
- addClassComponentRefreshWrapper,
addRefreshWrapper,
+ getPreambleCode,
preambleCode,
- runtimeCode,
runtimePublicPath,
-} from './fast-refresh'
+ silenceUseClientWarning,
+} from '@vitejs/react-common'
+import {
+ exactRegex,
+ makeIdFiltersToMatchWithQuery,
+} from '@rolldown/pluginutils'
+
+const _dirname = dirname(fileURLToPath(import.meta.url))
+const refreshRuntimePath = join(_dirname, 'refresh-runtime.js')
// lazy load babel since it's not used during build if plugins are not used
let babel: typeof babelCore | undefined
@@ -47,6 +50,14 @@ export interface Options {
babel?:
| BabelOptions
| ((id: string, options: { ssr?: boolean }) => BabelOptions)
+ /**
+ * React Fast Refresh runtime URL prefix.
+ * Useful in a module federation context to enable HMR by specifying
+ * the host application URL in the Vite config of a remote application.
+ * @example
+ * reactRefreshHost: 'http://localhost:3000'
+ */
+ reactRefreshHost?: string
}
export type BabelOptions = Omit<
@@ -87,21 +98,27 @@ export type ViteReactPluginApi = {
reactBabel?: ReactBabelHook
}
-const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
-const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
const defaultIncludeRE = /\.[tj]sx?$/
+const defaultExcludeRE = /\/node_modules\//
const tsRE = /\.tsx?$/
+const compilerAnnotationRE = /['"]use memo['"]/
+
+export default function viteReact(opts: Options = {}): Plugin[] {
+ const include = opts.include ?? defaultIncludeRE
+ const exclude = opts.exclude ?? defaultExcludeRE
+ const filter = createFilter(include, exclude)
-export default function viteReact(opts: Options = {}): PluginOption[] {
- // Provide default values for Rollup compat.
- let devBase = '/'
- const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude)
const jsxImportSource = opts.jsxImportSource ?? 'react'
const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`
+
+ const isRolldownVite = 'rolldownVersion' in vite
+ let runningInVite = false
let isProduction = true
let projectRoot = process.cwd()
- let skipFastRefresh = false
+ let skipFastRefresh = true
+ let base: string
+ let isFullBundle = false
let runPluginOverrides:
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
| undefined
@@ -116,7 +133,41 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
const viteBabel: Plugin = {
name: 'vite:react-babel',
enforce: 'pre',
- config() {
+ config(_userConfig, { command }) {
+ if ('rolldownVersion' in vite) {
+ if (opts.jsxRuntime === 'classic') {
+ return {
+ oxc: {
+ jsx: {
+ runtime: 'classic',
+ refresh: command === 'serve',
+ // disable __self and __source injection even in dev
+ // as this plugin injects them by babel and oxc will throw
+ // if development is enabled and those properties are already present
+ development: false,
+ },
+ jsxRefreshInclude: makeIdFiltersToMatchWithQuery(include),
+ jsxRefreshExclude: makeIdFiltersToMatchWithQuery(exclude),
+ },
+ }
+ } else {
+ return {
+ oxc: {
+ jsx: {
+ runtime: 'automatic',
+ importSource: opts.jsxImportSource,
+ refresh: command === 'serve',
+ },
+ jsxRefreshInclude: makeIdFiltersToMatchWithQuery(include),
+ jsxRefreshExclude: makeIdFiltersToMatchWithQuery(exclude),
+ },
+ optimizeDeps: {
+ rollupOptions: { transform: { jsx: { runtime: 'automatic' } } },
+ },
+ }
+ }
+ }
+
if (opts.jsxRuntime === 'classic') {
return {
esbuild: {
@@ -127,6 +178,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
return {
esbuild: {
jsx: 'automatic',
+ // keep undefined by default so that vite's esbuild transform can prioritize jsxImportSource from tsconfig
jsxImportSource: opts.jsxImportSource,
},
optimizeDeps: { esbuildOptions: { jsx: 'automatic' } },
@@ -134,7 +186,12 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
}
},
configResolved(config) {
- devBase = config.base
+ runningInVite = true
+ base = config.base
+ // @ts-expect-error only available in newer rolldown-vite
+ if (config.experimental.fullBundleMode) {
+ isFullBundle = true
+ }
projectRoot = config.root
isProduction = config.isProduction
skipFastRefresh =
@@ -142,12 +199,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
config.command === 'build' ||
config.server.hmr === false
- if ('jsxPure' in opts) {
- config.logger.warnOnce(
- '[@vitejs/plugin-react] jsxPure was removed. You can configure esbuild.jsxSideEffects directly.',
- )
- }
-
const hooks: ReactBabelHook[] = config.plugins
.map((plugin) => plugin.api?.reactBabel)
.filter(defined)
@@ -161,121 +212,270 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
// we only create static option in this case and re-create them
// each time otherwise
staticBabelOptions = createBabelOptions(opts.babel)
+
+ if (
+ (isRolldownVite || skipFastRefresh) &&
+ canSkipBabel(staticBabelOptions.plugins, staticBabelOptions) &&
+ (opts.jsxRuntime === 'classic' ? isProduction : true)
+ ) {
+ delete viteBabel.transform
+ }
}
},
- async transform(code, id, options) {
- if (id.includes('/node_modules/')) return
-
- const [filepath] = id.split('?')
- if (!filter(filepath)) return
-
- const ssr = options?.ssr === true
- const babelOptions = (() => {
- if (staticBabelOptions) return staticBabelOptions
- const newBabelOptions = createBabelOptions(
- typeof opts.babel === 'function'
- ? opts.babel(id, { ssr })
- : opts.babel,
- )
- runPluginOverrides?.(newBabelOptions, { id, ssr })
- return newBabelOptions
- })()
- const plugins = [...babelOptions.plugins]
-
- const isJSX = filepath.endsWith('x')
- const useFastRefresh =
- !skipFastRefresh &&
- !ssr &&
- (isJSX ||
- (opts.jsxRuntime === 'classic'
- ? importReactRE.test(code)
- : code.includes(jsxImportDevRuntime) ||
- code.includes(jsxImportRuntime)))
- if (useFastRefresh) {
- plugins.push([
- await loadPlugin('react-refresh/babel'),
- { skipEnvCheck: true },
- ])
+ options(options) {
+ if (!runningInVite) {
+ options.jsx = {
+ mode: opts.jsxRuntime,
+ importSource: opts.jsxImportSource,
+ }
+ return options
}
+ },
+ transform: {
+ filter: {
+ id: {
+ include: makeIdFiltersToMatchWithQuery(include),
+ exclude: makeIdFiltersToMatchWithQuery(exclude),
+ },
+ },
+ async handler(code, id, options) {
+ const [filepath] = id.split('?')
+ if (!filter(filepath)) return
+
+ const ssr = options?.ssr === true
+ const babelOptions = (() => {
+ if (staticBabelOptions) return staticBabelOptions
+ const newBabelOptions = createBabelOptions(
+ typeof opts.babel === 'function'
+ ? opts.babel(id, { ssr })
+ : opts.babel,
+ )
+ runPluginOverrides?.(newBabelOptions, { id, ssr })
+ return newBabelOptions
+ })()
+ const plugins = [...babelOptions.plugins]
+
+ // remove react-compiler plugin on non client environment
+ let reactCompilerPlugin = getReactCompilerPlugin(plugins)
+ if (reactCompilerPlugin && ssr) {
+ plugins.splice(plugins.indexOf(reactCompilerPlugin), 1)
+ reactCompilerPlugin = undefined
+ }
+
+ // filter by "use memo" when react-compiler { compilationMode: "annotation" }
+ // https://react.dev/learn/react-compiler/incremental-adoption#annotation-mode-configuration
+ if (
+ Array.isArray(reactCompilerPlugin) &&
+ reactCompilerPlugin[1]?.compilationMode === 'annotation' &&
+ !compilerAnnotationRE.test(code)
+ ) {
+ plugins.splice(plugins.indexOf(reactCompilerPlugin), 1)
+ reactCompilerPlugin = undefined
+ }
+
+ const isJSX = filepath.endsWith('x')
+ const useFastRefresh =
+ !(isRolldownVite || skipFastRefresh) &&
+ !ssr &&
+ (isJSX ||
+ (opts.jsxRuntime === 'classic'
+ ? importReactRE.test(code)
+ : code.includes(jsxImportDevRuntime) ||
+ code.includes(jsxImportRuntime)))
+ if (useFastRefresh) {
+ plugins.push([
+ await loadPlugin('react-refresh/babel'),
+ { skipEnvCheck: true },
+ ])
+ }
+
+ if (opts.jsxRuntime === 'classic' && isJSX) {
+ if (!isProduction) {
+ // These development plugins are only needed for the classic runtime.
+ plugins.push(
+ await loadPlugin('@babel/plugin-transform-react-jsx-self'),
+ await loadPlugin('@babel/plugin-transform-react-jsx-source'),
+ )
+ }
+ }
+
+ // Avoid parsing if no special transformation is needed
+ if (canSkipBabel(plugins, babelOptions)) {
+ return
+ }
- if (opts.jsxRuntime === 'classic' && isJSX) {
- if (!isProduction) {
- // These development plugins are only needed for the classic runtime.
- plugins.push(
- await loadPlugin('@babel/plugin-transform-react-jsx-self'),
- await loadPlugin('@babel/plugin-transform-react-jsx-source'),
+ const parserPlugins = [...babelOptions.parserOpts.plugins]
+
+ if (!filepath.endsWith('.ts')) {
+ parserPlugins.push('jsx')
+ }
+
+ if (tsRE.test(filepath)) {
+ parserPlugins.push('typescript')
+ }
+
+ const babel = await loadBabel()
+ const result = await babel.transformAsync(code, {
+ ...babelOptions,
+ root: projectRoot,
+ filename: id,
+ sourceFileName: filepath,
+ // Required for esbuild.jsxDev to provide correct line numbers
+ // This creates issues the react compiler because the re-order is too important
+ // People should use @babel/plugin-transform-react-jsx-development to get back good line numbers
+ retainLines: reactCompilerPlugin
+ ? false
+ : !isProduction && isJSX && opts.jsxRuntime !== 'classic',
+ parserOpts: {
+ ...babelOptions.parserOpts,
+ sourceType: 'module',
+ allowAwaitOutsideFunction: true,
+ plugins: parserPlugins,
+ },
+ generatorOpts: {
+ ...babelOptions.generatorOpts,
+ // import attributes parsing available without plugin since 7.26
+ importAttributesKeyword: 'with',
+ decoratorsBeforeExport: true,
+ },
+ plugins,
+ sourceMaps: true,
+ })
+
+ if (result) {
+ if (!useFastRefresh) {
+ return { code: result.code!, map: result.map }
+ }
+ const code = addRefreshWrapper(
+ result.code!,
+ '@vitejs/plugin-react',
+ id,
+ opts.reactRefreshHost,
)
+ return { code: code ?? result.code!, map: result.map }
}
+ },
+ },
+ }
+
+ // for rolldown-vite
+ const viteRefreshWrapper: Plugin = {
+ name: 'vite:react:refresh-wrapper',
+ apply: 'serve',
+ async applyToEnvironment(env) {
+ if (env.config.consumer !== 'client' || skipFastRefresh) {
+ return false
}
- // Avoid parsing if no special transformation is needed
+ let nativePlugin: ((options: any) => Plugin) | undefined
+ try {
+ // NOTE: `+` is to bypass lint & typecheck. vite/internal exists for newer rolldown-vite
+ const vite = 'vite'
+ nativePlugin = (await import(vite + '/internal'))
+ .reactRefreshWrapperPlugin
+ } catch {}
if (
- !plugins.length &&
- !babelOptions.presets.length &&
- !babelOptions.configFile &&
- !babelOptions.babelrc
+ !nativePlugin ||
+ ['7.1.10', '7.1.11', '7.1.12'].includes(vite.version)
) {
- return
- }
-
- const parserPlugins = [...babelOptions.parserOpts.plugins]
-
- if (!filepath.endsWith('.ts')) {
- parserPlugins.push('jsx')
+ // the native plugin in 7.1.10 and 7.1.11 and 7.1.12 does not support dev properly
+ return true
}
- if (tsRE.test(filepath)) {
- parserPlugins.push('typescript')
- }
+ delete viteRefreshWrapper.transform
- const babel = await loadBabel()
- const result = await babel.transformAsync(code, {
- ...babelOptions,
- root: projectRoot,
- filename: id,
- sourceFileName: filepath,
- // Required for esbuild.jsxDev to provide correct line numbers
- // This crates issues the react compiler because the re-order is too important
- // People should use @babel/plugin-transform-react-jsx-development to get back good line numbers
- retainLines: hasCompiler(plugins)
- ? false
- : !isProduction && isJSX && opts.jsxRuntime !== 'classic',
- parserOpts: {
- ...babelOptions.parserOpts,
- sourceType: 'module',
- allowAwaitOutsideFunction: true,
- plugins: parserPlugins,
- },
- generatorOpts: {
- ...babelOptions.generatorOpts,
- decoratorsBeforeExport: true,
+ return nativePlugin({
+ cwd: process.cwd(),
+ include: makeIdFiltersToMatchWithQuery(include),
+ exclude: makeIdFiltersToMatchWithQuery(exclude),
+ jsxImportSource,
+ reactRefreshHost: opts.reactRefreshHost ?? '',
+ }) as unknown as boolean
+ },
+ // we can remove this transform hook when we drop support for rolldown-vite 7.1.12 and below
+ transform: {
+ filter: {
+ id: {
+ include: makeIdFiltersToMatchWithQuery(include),
+ exclude: makeIdFiltersToMatchWithQuery(exclude),
},
- plugins,
- sourceMaps: true,
- })
+ },
+ handler(code, id, options) {
+ const ssr = options?.ssr === true
+
+ const [filepath] = id.split('?')
+ const isJSX = filepath.endsWith('x')
+ const useFastRefresh =
+ !skipFastRefresh &&
+ !ssr &&
+ (isJSX ||
+ code.includes(jsxImportDevRuntime) ||
+ code.includes(jsxImportRuntime))
+ if (!useFastRefresh) return
+
+ const newCode = addRefreshWrapper(
+ code,
+ '@vitejs/plugin-react',
+ id,
+ opts.reactRefreshHost,
+ )
+ return newCode ? { code: newCode, map: null } : undefined
+ },
+ },
+ }
- if (result) {
- let code = result.code!
- if (useFastRefresh) {
- if (refreshContentRE.test(code)) {
- code = addRefreshWrapper(code, id)
- } else if (reactCompRE.test(code)) {
- code = addClassComponentRefreshWrapper(code, id)
- }
- }
- return { code, map: result.map }
+ // for rolldown-vite
+ const viteConfigPost: Plugin = {
+ name: 'vite:react:config-post',
+ enforce: 'post',
+ config(userConfig) {
+ if (userConfig.server?.hmr === false) {
+ return {
+ oxc: {
+ jsx: {
+ refresh: false,
+ },
+ },
+ // oxc option is only available in rolldown-vite
+ } as any
}
},
}
- // We can't add `react-dom` because the dependency is `react-dom/client`
- // for React 18 while it's `react-dom` for React 17. We'd need to detect
- // what React version the user has installed.
- const dependencies = ['react', jsxImportDevRuntime, jsxImportRuntime]
+ // for rolldown-vite + full bundle mode
+ const viteReactRefreshFullBundleMode: Plugin = {
+ name: 'vite:react-refresh-fbm',
+ enforce: 'pre',
+ transformIndexHtml: {
+ handler() {
+ if (!skipFastRefresh && isFullBundle)
+ return [
+ {
+ tag: 'script',
+ attrs: { type: 'module' },
+ children: getPreambleCode(base),
+ },
+ ]
+ },
+ // In unbundled mode, Vite transforms any requests.
+ // But in full bundled mode, Vite only transforms / bundles the scripts injected in `order: 'pre'`.
+ order: 'pre',
+ },
+ }
+
+ const dependencies = [
+ 'react',
+ 'react-dom',
+ jsxImportDevRuntime,
+ jsxImportRuntime,
+ ]
const staticBabelPlugins =
- typeof opts.babel === 'object' ? opts.babel?.plugins ?? [] : []
- if (hasCompilerWithDefaultRuntime(staticBabelPlugins)) {
- dependencies.push('react/compiler-runtime')
+ typeof opts.babel === 'object' ? (opts.babel?.plugins ?? []) : []
+ const reactCompilerPlugin = getReactCompilerPlugin(staticBabelPlugins)
+ if (reactCompilerPlugin != null) {
+ const reactCompilerRuntimeModule =
+ getReactCompilerRuntimeModule(reactCompilerPlugin)
+ dependencies.push(reactCompilerRuntimeModule)
}
const viteReactRefresh: Plugin = {
@@ -286,54 +486,69 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
optimizeDeps: {
include: dependencies,
},
- resolve: {
- dedupe: ['react', 'react-dom'],
- },
}),
- resolveId(id) {
- if (id === runtimePublicPath) {
- return id
- }
+ resolveId: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler(id) {
+ if (id === runtimePublicPath) {
+ return id
+ }
+ },
},
- load(id) {
- if (id === runtimePublicPath) {
- return runtimeCode
- }
+ load: {
+ filter: { id: exactRegex(runtimePublicPath) },
+ handler(id) {
+ if (id === runtimePublicPath) {
+ return readFileSync(refreshRuntimePath, 'utf-8').replace(
+ /__README_URL__/g,
+ 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react',
+ )
+ }
+ },
},
transformIndexHtml() {
- if (!skipFastRefresh)
+ if (!skipFastRefresh && !isFullBundle)
return [
{
tag: 'script',
attrs: { type: 'module' },
- children: preambleCode.replace(`__BASE__`, devBase),
+ children: getPreambleCode(base),
},
]
},
}
- return [viteBabel, viteReactRefresh]
+ return [
+ viteBabel,
+ ...(isRolldownVite
+ ? [viteRefreshWrapper, viteConfigPost, viteReactRefreshFullBundleMode]
+ : []),
+ viteReactRefresh,
+ ]
}
viteReact.preambleCode = preambleCode
-const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({
- rollupOptions: {
- onwarn(warning, defaultHandler) {
- if (
- warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
- warning.message.includes('use client')
- ) {
- return
- }
- if (userConfig.build?.rollupOptions?.onwarn) {
- userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
- } else {
- defaultHandler(warning)
- }
- },
- },
+// Compat for require
+function viteReactForCjs(this: unknown, options: Options): Plugin[] {
+ return viteReact.call(this, options)
+}
+Object.assign(viteReactForCjs, {
+ default: viteReactForCjs,
})
+export { viteReactForCjs as 'module.exports' }
+
+function canSkipBabel(
+ plugins: ReactBabelOptions['plugins'],
+ babelOptions: ReactBabelOptions,
+) {
+ return !(
+ plugins.length ||
+ babelOptions.presets.length ||
+ babelOptions.configFile ||
+ babelOptions.babelrc
+ )
+}
const loadedPlugin = new Map()
function loadPlugin(path: string): any {
@@ -369,21 +584,25 @@ function defined(value: T | undefined): value is T {
return value !== undefined
}
-function hasCompiler(plugins: ReactBabelOptions['plugins']) {
- return plugins.some(
+function getReactCompilerPlugin(plugins: ReactBabelOptions['plugins']) {
+ return plugins.find(
(p) =>
p === 'babel-plugin-react-compiler' ||
(Array.isArray(p) && p[0] === 'babel-plugin-react-compiler'),
)
}
-// https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43
-function hasCompilerWithDefaultRuntime(plugins: ReactBabelOptions['plugins']) {
- return plugins.some(
- (p) =>
- p === 'babel-plugin-react-compiler' ||
- (Array.isArray(p) &&
- p[0] === 'babel-plugin-react-compiler' &&
- p[1]?.runtimeModule === undefined),
- )
+type ReactCompilerRuntimeModule =
+ | 'react/compiler-runtime' // from react namespace
+ | 'react-compiler-runtime' // npm package
+function getReactCompilerRuntimeModule(
+ plugin: babelCore.PluginItem,
+): ReactCompilerRuntimeModule {
+ let moduleName: ReactCompilerRuntimeModule = 'react/compiler-runtime'
+ if (Array.isArray(plugin)) {
+ if (plugin[1]?.target === '17' || plugin[1]?.target === '18') {
+ moduleName = 'react-compiler-runtime'
+ }
+ }
+ return moduleName
}
diff --git a/packages/plugin-react/src/refreshUtils.js b/packages/plugin-react/src/refreshUtils.js
deleted file mode 100644
index 0ca2b0117..000000000
--- a/packages/plugin-react/src/refreshUtils.js
+++ /dev/null
@@ -1,71 +0,0 @@
-function debounce(fn, delay) {
- let handle
- return () => {
- clearTimeout(handle)
- handle = setTimeout(fn, delay)
- }
-}
-
-/* eslint-disable no-undef */
-const enqueueUpdate = debounce(exports.performReactRefresh, 16)
-
-// Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141
-// This allows to resister components not detected by SWC like styled component
-function registerExportsForReactRefresh(filename, moduleExports) {
- for (const key in moduleExports) {
- if (key === '__esModule') continue
- const exportValue = moduleExports[key]
- if (exports.isLikelyComponentType(exportValue)) {
- // 'export' is required to avoid key collision when renamed exports that
- // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116
- // The register function has an identity check to not register twice the same component,
- // so this is safe to not used the same key here.
- exports.register(exportValue, filename + ' export ' + key)
- }
- }
-}
-
-function validateRefreshBoundaryAndEnqueueUpdate(prevExports, nextExports) {
- if (!predicateOnExport(prevExports, (key) => key in nextExports)) {
- return 'Could not Fast Refresh (export removed)'
- }
- if (!predicateOnExport(nextExports, (key) => key in prevExports)) {
- return 'Could not Fast Refresh (new export)'
- }
-
- let hasExports = false
- const allExportsAreComponentsOrUnchanged = predicateOnExport(
- nextExports,
- (key, value) => {
- hasExports = true
- if (exports.isLikelyComponentType(value)) return true
- return prevExports[key] === nextExports[key]
- },
- )
- if (hasExports && allExportsAreComponentsOrUnchanged) {
- enqueueUpdate()
- } else {
- return 'Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports'
- }
-}
-
-function predicateOnExport(moduleExports, predicate) {
- for (const key in moduleExports) {
- if (key === '__esModule') continue
- const desc = Object.getOwnPropertyDescriptor(moduleExports, key)
- if (desc && desc.get) return false
- if (!predicate(key, moduleExports[key])) return false
- }
- return true
-}
-
-// Hides vite-ignored dynamic import so that Vite can skip analysis if no other
-// dynamic import is present (https://github.com/vitejs/vite/pull/12732)
-function __hmr_import(module) {
- return import(/* @vite-ignore */ module)
-}
-
-exports.__hmr_import = __hmr_import
-exports.registerExportsForReactRefresh = registerExportsForReactRefresh
-exports.validateRefreshBoundaryAndEnqueueUpdate =
- validateRefreshBoundaryAndEnqueueUpdate
diff --git a/packages/plugin-react/tests/rolldown.test.ts b/packages/plugin-react/tests/rolldown.test.ts
new file mode 100644
index 000000000..e62e7cb53
--- /dev/null
+++ b/packages/plugin-react/tests/rolldown.test.ts
@@ -0,0 +1,66 @@
+import path from 'node:path'
+import { expect, test } from 'vitest'
+import { type Plugin, rolldown } from 'rolldown'
+import pluginReact, { type Options } from '../src/index.ts'
+
+test('HMR related code should not be included when using rolldown', async () => {
+ const { output } = await bundleWithRolldown()
+
+ expect(output[0].code).toBeDefined()
+ expect(output[0].code).not.toContain('import.meta.hot')
+})
+
+test('HMR related code should not be included when using rolldown with babel plugin', async () => {
+ const { output } = await bundleWithRolldown({
+ babel: {
+ plugins: [['babel-plugin-react-compiler', {}]],
+ },
+ })
+
+ expect(output[0].code).toBeDefined()
+ expect(output[0].code).not.toContain('import.meta.hot')
+})
+
+async function bundleWithRolldown(pluginReactOptions: Options = {}) {
+ const ENTRY = '/entry.tsx'
+ const files: Record = {
+ [ENTRY]: /* tsx */ `
+ import React from "react"
+ import { hydrateRoot } from "react-dom/client"
+ import App from "./App.tsx"
+
+ const container = document.getElementById("root");
+ hydrateRoot(container, );
+ `,
+ '/App.tsx': /* tsx */ `
+ export default function App() {
+ return
Hello World
+ }
+ `,
+ }
+
+ const bundle = await rolldown({
+ input: ENTRY,
+ plugins: [virtualFilesPlugin(files), pluginReact(pluginReactOptions)],
+ external: [/^react(\/|$)/, /^react-dom(\/|$)/],
+ })
+ return await bundle.generate({ format: 'esm' })
+}
+
+function virtualFilesPlugin(files: Record): Plugin {
+ return {
+ name: 'virtual-files',
+ resolveId(id, importer) {
+ const baseDir = importer ? path.posix.dirname(importer) : '/'
+ const result = path.posix.resolve(baseDir, id)
+ if (result in files) {
+ return result
+ }
+ },
+ load(id) {
+ if (id in files) {
+ return files[id]
+ }
+ },
+ }
+}
diff --git a/packages/plugin-react/tsconfig.json b/packages/plugin-react/tsconfig.json
index e2b17f9c7..70c7eacff 100644
--- a/packages/plugin-react/tsconfig.json
+++ b/packages/plugin-react/tsconfig.json
@@ -1,9 +1,9 @@
{
- "include": ["src", "scripts"],
+ "include": ["src"],
"compilerOptions": {
"outDir": "dist",
- "target": "ES2020",
- "module": "ES2020",
+ "target": "es2023",
+ "module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
diff --git a/packages/plugin-react/tsdown.config.ts b/packages/plugin-react/tsdown.config.ts
new file mode 100644
index 000000000..3e38aa5d7
--- /dev/null
+++ b/packages/plugin-react/tsdown.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'tsdown'
+
+export default defineConfig({
+ entry: 'src/index.ts',
+ dts: true,
+ copy: [
+ {
+ from: 'node_modules/@vitejs/react-common/refresh-runtime.js',
+ to: 'dist/refresh-runtime.js',
+ },
+ ],
+})
diff --git a/packages/plugin-rsc/.gitignore b/packages/plugin-rsc/.gitignore
new file mode 100644
index 000000000..416330c31
--- /dev/null
+++ b/packages/plugin-rsc/.gitignore
@@ -0,0 +1,13 @@
+node_modules
+dist
+.vercel
+.vite-node
+.wrangler
+.netlify
+*.log
+*.tgz
+test-results
+*.tsbuildinfo
+.debug
+.vite-inspect
+.claude
diff --git a/packages/plugin-rsc/AGENTS.md b/packages/plugin-rsc/AGENTS.md
new file mode 100644
index 000000000..1b75ef7b1
--- /dev/null
+++ b/packages/plugin-rsc/AGENTS.md
@@ -0,0 +1,19 @@
+# AI Agent Guide for @vitejs/plugin-rsc
+
+This document provides AI-agent-specific guidance for the React Server Components (RSC) plugin. For comprehensive documentation, see:
+
+- **[README.md](README.md)** - Plugin overview, concepts, and examples
+- **[CONTRIBUTING.md](CONTRIBUTING.md)** - Development setup and testing guidelines
+
+## Quick Reference for AI Agents
+
+### Essential Commands
+
+```bash
+# inside packages/plugin-rsc directory
+pnpm build # build package
+pnpm tsc # typecheck
+pnpm dev # Watch mode development
+pnpm test-e2e # Run e2e tests
+pnpm test-e2e basic # Test specific example
+```
diff --git a/packages/plugin-rsc/CHANGELOG.md b/packages/plugin-rsc/CHANGELOG.md
new file mode 100644
index 000000000..54fb7cfe9
--- /dev/null
+++ b/packages/plugin-rsc/CHANGELOG.md
@@ -0,0 +1,608 @@
+## [0.4.33](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.32...plugin-rsc@0.4.33) (2025-10-08)
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#887](https://github.com/vitejs/vite-plugin-react/issues/887)) ([407795d](https://github.com/vitejs/vite-plugin-react/commit/407795dbd0129b069cf3ac842846687485a5ef00))
+* **deps:** update all non-major dependencies ([#896](https://github.com/vitejs/vite-plugin-react/issues/896)) ([2d239fc](https://github.com/vitejs/vite-plugin-react/commit/2d239fc8dec2ab499282eaea45b2bffb8d182f26))
+* **rsc/cjs:** add `__filename` and `__dirname` ([#908](https://github.com/vitejs/vite-plugin-react/issues/908)) ([0ba0d71](https://github.com/vitejs/vite-plugin-react/commit/0ba0d71bc92822946f327760691db3d6f7d87106))
+* **rsc/cjs:** unwrap `default` based on `__cjs_module_runner_transform` marker ([#905](https://github.com/vitejs/vite-plugin-react/issues/905)) ([1216caf](https://github.com/vitejs/vite-plugin-react/commit/1216caf70621b8760c4226624939b77e7ece4f42))
+
+### Code Refactoring
+
+* **rsc:** move common code for `transformCjsToEsm` ([#909](https://github.com/vitejs/vite-plugin-react/issues/909)) ([ac61c62](https://github.com/vitejs/vite-plugin-react/commit/ac61c624d8a7f860af735ad288491b5c50c656bb))
+
+## [0.4.32](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.31...plugin-rsc@0.4.32) (2025-09-26)
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#851](https://github.com/vitejs/vite-plugin-react/issues/851)) ([3c2ebf8](https://github.com/vitejs/vite-plugin-react/commit/3c2ebf89de7f5e40ed0ef932993f7d0b7695719b))
+* **rsc:** reject inline "use server" inside "use client" module ([#884](https://github.com/vitejs/vite-plugin-react/issues/884)) ([5bc3f79](https://github.com/vitejs/vite-plugin-react/commit/5bc3f79fb4356ebf574b6ba28e4c7a315f4336de))
+
+### Miscellaneous Chores
+
+* **deps:** update all non-major dependencies ([#879](https://github.com/vitejs/vite-plugin-react/issues/879)) ([608f266](https://github.com/vitejs/vite-plugin-react/commit/608f266c8d53f41a6b1541de35b218fe2640ec05))
+* **rsc:** fix typo ([#885](https://github.com/vitejs/vite-plugin-react/issues/885)) ([b81470c](https://github.com/vitejs/vite-plugin-react/commit/b81470c3076e079be517b7bf92325760ba89fd3d))
+
+## [0.4.31](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.30...plugin-rsc@0.4.31) (2025-09-17)
+### Bug Fixes
+
+* **rsc:** fix plugin name in `client-only` error message ([#862](https://github.com/vitejs/vite-plugin-react/issues/862)) ([0f2fbc7](https://github.com/vitejs/vite-plugin-react/commit/0f2fbc7c1fe2b6228864c5b424cea2309323fb67))
+* **rsc:** remove server style when css import is removed ([#849](https://github.com/vitejs/vite-plugin-react/issues/849)) ([4ae3f18](https://github.com/vitejs/vite-plugin-react/commit/4ae3f184eaa3e7fe559ccaf67c35e17a6e7fefa0))
+* **rsc:** show import chain for server-only and client-only import error ([#867](https://github.com/vitejs/vite-plugin-react/issues/867)) ([ba16c34](https://github.com/vitejs/vite-plugin-react/commit/ba16c34f6b70e68f29bff110c6906829ec3b2e8d))
+
+### Documentation
+
+* **rsc:** mention `validateImports` option for build time `server-only` and `client-only` validation ([#858](https://github.com/vitejs/vite-plugin-react/issues/858)) ([a96a6b2](https://github.com/vitejs/vite-plugin-react/commit/a96a6b2ef0afc1cc914885d4514865711d978fbf))
+* **rsc:** separate "Tips" section ([#864](https://github.com/vitejs/vite-plugin-react/issues/864)) ([32cfa5f](https://github.com/vitejs/vite-plugin-react/commit/32cfa5fe1e9c255e59a29c14d1b8585772f7b61e))
+
+### Miscellaneous Chores
+
+* **rsc:** add missing rsc-html-stream dep (fix [#857](https://github.com/vitejs/vite-plugin-react/issues/857)) ([#868](https://github.com/vitejs/vite-plugin-react/issues/868)) ([c30cf1a](https://github.com/vitejs/vite-plugin-react/commit/c30cf1a7db312a2643de426c7ac13479ce90289a))
+
+### Tests
+
+* **rsc:** tweak assertions for rolldown-vite ([#869](https://github.com/vitejs/vite-plugin-react/issues/869)) ([a2a287a](https://github.com/vitejs/vite-plugin-react/commit/a2a287aef6be302a771b8f7c512f190578412685))
+
+## [0.4.30](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.29...plugin-rsc@0.4.30) (2025-09-15)
+### Features
+
+* **rsc:** support `export default { fetch }` as server handler entry ([#839](https://github.com/vitejs/vite-plugin-react/issues/839)) ([cb5ce55](https://github.com/vitejs/vite-plugin-react/commit/cb5ce555e234166022dd899c71c88ad3eb7e5192))
+
+### Bug Fixes
+
+* **rsc:** `copyPublicDir: false` for server build ([#831](https://github.com/vitejs/vite-plugin-react/issues/831)) ([12b05bb](https://github.com/vitejs/vite-plugin-react/commit/12b05bb3ec0155459b205199432b35e05ef3594a))
+* **rsc:** fix cjs transform to preserve `module.exports` on `require` side and allow `exports` assignment + expose `cjsModuleRunnerPlugin` ([#833](https://github.com/vitejs/vite-plugin-react/issues/833)) ([f63bb83](https://github.com/vitejs/vite-plugin-react/commit/f63bb83c7070d07ae5f488cdc9ac643bac61ba59))
+* **rsc:** keep server stylesheet link for hmr and avoid injecting css via client js ([#841](https://github.com/vitejs/vite-plugin-react/issues/841)) ([2b7b90f](https://github.com/vitejs/vite-plugin-react/commit/2b7b90f9ee94ca70beda90f288df2a5b6b260900))
+
+### Documentation
+
+* **rsc:** remove unimportant APIs ([#830](https://github.com/vitejs/vite-plugin-react/issues/830)) ([9cabda1](https://github.com/vitejs/vite-plugin-react/commit/9cabda1574f95a123ba5f90ed94ed9bc9f8f04fc))
+* **rsc:** replace degit with create-vite ([#846](https://github.com/vitejs/vite-plugin-react/issues/846)) ([7c3edba](https://github.com/vitejs/vite-plugin-react/commit/7c3edba29b4996a77862c7dc7cb47bf51418dcd0))
+
+### Miscellaneous Chores
+
+* **rsc:** remove double `import.meta.hot.accept` ([#840](https://github.com/vitejs/vite-plugin-react/issues/840)) ([a4bc2e0](https://github.com/vitejs/vite-plugin-react/commit/a4bc2e0c6cf7426dcb7b8b2945ca46377a7db688))
+
+### Code Refactoring
+
+* **rsc:** self-accept css module direct request module on client environment ([#842](https://github.com/vitejs/vite-plugin-react/issues/842)) ([e37788b](https://github.com/vitejs/vite-plugin-react/commit/e37788bbde37daa9f6954891e90832566e65a667))
+* **rsc:** use `addWatchFile` to invalidate server css virtual ([#847](https://github.com/vitejs/vite-plugin-react/issues/847)) ([78a3f56](https://github.com/vitejs/vite-plugin-react/commit/78a3f56002d98f609998fd2cdad8e0299080cb8b))
+
+### Tests
+
+* **rsc:** fix renderBuiltUrl runtime for css ([#838](https://github.com/vitejs/vite-plugin-react/issues/838)) ([19d14c2](https://github.com/vitejs/vite-plugin-react/commit/19d14c220bc66b1d985f5e018876dc5d5ff7b5ce))
+* **rsc:** test adding css import works without reload ([#845](https://github.com/vitejs/vite-plugin-react/issues/845)) ([eab0a16](https://github.com/vitejs/vite-plugin-react/commit/eab0a16986d6cd6cd70621c5b1bf18b6d4425ca8))
+* **rsc:** tweak timeout ([#854](https://github.com/vitejs/vite-plugin-react/issues/854)) ([456449d](https://github.com/vitejs/vite-plugin-react/commit/456449d5c757f3fea51976b6c92ffd69ec767640))
+
+## [0.4.29](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.28...plugin-rsc@0.4.29) (2025-09-09)
+### Features
+
+* **rsc:** expose `transforms` utils ([#828](https://github.com/vitejs/vite-plugin-react/issues/828)) ([0a8e4dc](https://github.com/vitejs/vite-plugin-react/commit/0a8e4dcb664d728dbb41bd3ec12b3d258176dd7b))
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#823](https://github.com/vitejs/vite-plugin-react/issues/823)) ([afa28f1](https://github.com/vitejs/vite-plugin-react/commit/afa28f1675e8169f6494413b2bb69577b9cbf6f5))
+* **rsc:** fix build error when entire client reference module is tree-shaken ([#827](https://github.com/vitejs/vite-plugin-react/issues/827)) ([f515bd8](https://github.com/vitejs/vite-plugin-react/commit/f515bd8d82122ba4a2a80886978270182fd7bcbb))
+
+### Code Refactoring
+
+* **rsc:** remove top-level `transformHoistInlineDirective` export in favor of `@vitejs/plugin-rsc/transforms` ([#829](https://github.com/vitejs/vite-plugin-react/issues/829)) ([3122b0d](https://github.com/vitejs/vite-plugin-react/commit/3122b0d25e01206fb52e8c9eb30cc894126f02cf))
+
+## [0.4.28](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.27...plugin-rsc@0.4.28) (2025-09-08)
+### Features
+
+* **rsc:** support browser mode build ([#801](https://github.com/vitejs/vite-plugin-react/issues/801)) ([b81bf6a](https://github.com/vitejs/vite-plugin-react/commit/b81bf6ac8a273855c5e9f39d71a32d76fd31b61c))
+
+### Bug Fixes
+
+* **rsc:** support `rsc.loadModuleDevProxy` top-level config ([#825](https://github.com/vitejs/vite-plugin-react/issues/825)) ([d673dd0](https://github.com/vitejs/vite-plugin-react/commit/d673dd0a525a9baf6644a89f28cd1537847741bb))
+
+### Miscellaneous Chores
+
+* add AGENTS.md documentation for AI agent development guidance ([#820](https://github.com/vitejs/vite-plugin-react/issues/820)) ([d1627cb](https://github.com/vitejs/vite-plugin-react/commit/d1627cbdd20ac2ce1f91185ef0ba1be882a0186b))
+
+### Tests
+
+* **rsc:** test `useId` ([#818](https://github.com/vitejs/vite-plugin-react/issues/818)) ([768cfd3](https://github.com/vitejs/vite-plugin-react/commit/768cfd3c7fd956497ec5e39734c0c1a62a2a441c))
+* **rsc:** test middleware mode ([#817](https://github.com/vitejs/vite-plugin-react/issues/817)) ([4672651](https://github.com/vitejs/vite-plugin-react/commit/467265104995f9b07058269f2905a78a9cc0c2ce))
+
+## [0.4.27](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.26...plugin-rsc@0.4.27) (2025-09-01)
+### Features
+
+* **rsc:** enable `buildApp` plugin hook by default for Vite 7 ([#815](https://github.com/vitejs/vite-plugin-react/issues/815)) ([0a02b83](https://github.com/vitejs/vite-plugin-react/commit/0a02b835efb8de7ff2f95008a5321738b9b6a0b0))
+* **rsc:** support `UserConfig.rsc: RscPluginOptions` ([#810](https://github.com/vitejs/vite-plugin-react/issues/810)) ([07a64c2](https://github.com/vitejs/vite-plugin-react/commit/07a64c25ab056689c99ce348810aa721a7f1926b))
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#809](https://github.com/vitejs/vite-plugin-react/issues/809)) ([437bab2](https://github.com/vitejs/vite-plugin-react/commit/437bab254d1f1fa3542dd335c6763ee36c8826be))
+* **rsc:** delay `validateImportPlugin` setup ([#813](https://github.com/vitejs/vite-plugin-react/issues/813)) ([4da5810](https://github.com/vitejs/vite-plugin-react/commit/4da58106e9c2ba1258ff3f97e853324af24f4ed8))
+
+### Documentation
+
+* **rsc:** mention `@vitejs/plugin-rsc/types` ([#816](https://github.com/vitejs/vite-plugin-react/issues/816)) ([3568e89](https://github.com/vitejs/vite-plugin-react/commit/3568e890d21c8cc80ef901222f1f04ca0dbdc1c5))
+
+### Miscellaneous Chores
+
+* fix type in `README.md` ([#804](https://github.com/vitejs/vite-plugin-react/issues/804)) ([f9d7cd9](https://github.com/vitejs/vite-plugin-react/commit/f9d7cd96bdd86b63dc028daf6731860e13a5d3bf))
+
+### Code Refactoring
+
+* **rsc:** simplify `validateImportPlugin` ([#814](https://github.com/vitejs/vite-plugin-react/issues/814)) ([3969f86](https://github.com/vitejs/vite-plugin-react/commit/3969f8602cf43de95d6ae086a0612188d56a239d))
+
+## [0.4.26](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.25...plugin-rsc@0.4.26) (2025-08-29)
+### Features
+
+* **rsc:** enable server-chunk-based client chunks ([#794](https://github.com/vitejs/vite-plugin-react/issues/794)) ([377a273](https://github.com/vitejs/vite-plugin-react/commit/377a273b27d0996ae9d2be50a74dc372d91cdc9c))
+
+### Bug Fixes
+
+* **rsc:** use `req.originalUrl` for server handler ([#797](https://github.com/vitejs/vite-plugin-react/issues/797)) ([3250231](https://github.com/vitejs/vite-plugin-react/commit/3250231b7537daf6946a27ec8bd8dc47a646d034))
+
+### Documentation
+
+* **rsc:** how to use `@vitejs/plugin-rsc` as framework's `dependencies` ([#796](https://github.com/vitejs/vite-plugin-react/issues/796)) ([907b9d8](https://github.com/vitejs/vite-plugin-react/commit/907b9d8323e7a21160a58d328d6ac444e5fa31da))
+
+### Miscellaneous Chores
+
+* **rsc:** typo in viteRscAsyncHooks naming ([#793](https://github.com/vitejs/vite-plugin-react/issues/793)) ([95e4091](https://github.com/vitejs/vite-plugin-react/commit/95e4091dcb973506136bd1564000916e8a38c440))
+
+### Code Refactoring
+
+* **rsc:** organize internal plugins ([#791](https://github.com/vitejs/vite-plugin-react/issues/791)) ([d8cfdfa](https://github.com/vitejs/vite-plugin-react/commit/d8cfdfa1b8aca65fae2e555b0ae8a66eb9276ed6))
+
+## [0.4.25](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.24...plugin-rsc@0.4.25) (2025-08-28)
+### Bug Fixes
+
+* **rsc:** inject `AsyncLocalStorage` global via transform ([#785](https://github.com/vitejs/vite-plugin-react/issues/785)) ([2f255ad](https://github.com/vitejs/vite-plugin-react/commit/2f255ad694b976ff0b6f826f5fe8c27da5852df1))
+* **rsc:** optimize `react-dom/static.edge` ([#786](https://github.com/vitejs/vite-plugin-react/issues/786)) ([e3bf733](https://github.com/vitejs/vite-plugin-react/commit/e3bf73356bf307d68e5e62c06987815afb1a1f44))
+* **rsc:** propagate client reference invalidation to server ([#788](https://github.com/vitejs/vite-plugin-react/issues/788)) ([a8dc3fe](https://github.com/vitejs/vite-plugin-react/commit/a8dc3feade6fc64b1cfd851d90b39d4d7ba98b02))
+
+### Miscellaneous Chores
+
+* **deps:** update `@types/react-dom` to fix `formState` ([#782](https://github.com/vitejs/vite-plugin-react/issues/782)) ([af9139f](https://github.com/vitejs/vite-plugin-react/commit/af9139f0bf1e30d4ffbd23b065001b0284cfda05))
+
+### Tests
+
+* **rsc:** test `hydrateRoot(..., { formState })` ([#781](https://github.com/vitejs/vite-plugin-react/issues/781)) ([e622a6a](https://github.com/vitejs/vite-plugin-react/commit/e622a6a06b4d021430a42defe893353940931915))
+
+## [0.4.24](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.23...plugin-rsc@0.4.24) (2025-08-25)
+### Features
+
+* **rsc:** ability to merge client reference chunks ([#766](https://github.com/vitejs/vite-plugin-react/issues/766)) ([c40234e](https://github.com/vitejs/vite-plugin-react/commit/c40234ef079e5e27e86acf88c8c987db8bb1b16c))
+* **rsc:** ability to merge client reference chunks based on server chunk usage ([#767](https://github.com/vitejs/vite-plugin-react/issues/767)) ([c69f0f6](https://github.com/vitejs/vite-plugin-react/commit/c69f0f6b834ac518f183b0a76851d17ddb7a81d0))
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#773](https://github.com/vitejs/vite-plugin-react/issues/773)) ([9989897](https://github.com/vitejs/vite-plugin-react/commit/9989897fd102ba2d46bee0961e43aacb1e4f9436))
+* **rsc:** fix client reference preload when group chunk re-exports client components from entry chunk ([#768](https://github.com/vitejs/vite-plugin-react/issues/768)) ([41e4bf5](https://github.com/vitejs/vite-plugin-react/commit/41e4bf586c7ebd81fba9e25e72c90386b5e88a4d))
+* **rsc:** fix CSS HMR with `?url` ([#776](https://github.com/vitejs/vite-plugin-react/issues/776)) ([4c4879b](https://github.com/vitejs/vite-plugin-react/commit/4c4879b0c1080b536ac6521a7030691a06469b3a))
+* **rsc:** normalize group chunk virtual id properly ([#770](https://github.com/vitejs/vite-plugin-react/issues/770)) ([9869e2c](https://github.com/vitejs/vite-plugin-react/commit/9869e2c7c51b3f001389255dbc40beafb76cac7b))
+
+### Miscellaneous Chores
+
+* **rsc:** custom client chunks example ([#765](https://github.com/vitejs/vite-plugin-react/issues/765)) ([6924db4](https://github.com/vitejs/vite-plugin-react/commit/6924db40f5cbfb9e02f4e4c5beacc2671f4df0ee))
+* **rsc:** fix `useBuildAppHook: true` with cloudflare plugin ([#780](https://github.com/vitejs/vite-plugin-react/issues/780)) ([8fec8e3](https://github.com/vitejs/vite-plugin-react/commit/8fec8e3b79cce570fb369b6bddd35938ad2ec37a))
+
+### Code Refactoring
+
+* **rsc:** add `toRelativeId` util ([#771](https://github.com/vitejs/vite-plugin-react/issues/771)) ([d9da80f](https://github.com/vitejs/vite-plugin-react/commit/d9da80ffa804ea839a99e331b2dd33b9478a7d76))
+* **rsc:** organize plugin utils ([#779](https://github.com/vitejs/vite-plugin-react/issues/779)) ([789e359](https://github.com/vitejs/vite-plugin-react/commit/789e3592d756227739b2285bda95a5d5dc9e5e93))
+
+### Tests
+
+* **rsc:** organize css tests ([#778](https://github.com/vitejs/vite-plugin-react/issues/778)) ([e71da84](https://github.com/vitejs/vite-plugin-react/commit/e71da842f89fdb0c549e874205e65601109f41b9))
+
+## [0.4.23](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.22...plugin-rsc@0.4.23) (2025-08-23)
+### Bug Fixes
+
+* **rsc:** replace `'rolldownVersion' in this.meta` with `'rolldownVersion' in vite` for Vite 6 compat ([#761](https://github.com/vitejs/vite-plugin-react/issues/761)) ([af4e16d](https://github.com/vitejs/vite-plugin-react/commit/af4e16da970f2808e0ab4484500f0a038c8b176a))
+
+### Miscellaneous Chores
+
+* **rsc:** remove custom `react-dom/server.edge` types ([#757](https://github.com/vitejs/vite-plugin-react/issues/757)) ([a7ca366](https://github.com/vitejs/vite-plugin-react/commit/a7ca366f57f97ea0ab540dce645095ed9efedce8))
+* **rsc:** simplify react-router example ([#763](https://github.com/vitejs/vite-plugin-react/issues/763)) ([22f6538](https://github.com/vitejs/vite-plugin-react/commit/22f6538ea1536700da8588f4d9960787f51f1bcd))
+* **rsc:** use `prerender` in ssg example ([#758](https://github.com/vitejs/vite-plugin-react/issues/758)) ([df8b800](https://github.com/vitejs/vite-plugin-react/commit/df8b80055c567b0248c506e2c57fb613d9da128f))
+
+### Tests
+
+* **rsc:** test vite 6 ([#762](https://github.com/vitejs/vite-plugin-react/issues/762)) ([a46bdf4](https://github.com/vitejs/vite-plugin-react/commit/a46bdf45712e144c07797844b31e98bec5154be4))
+
+## [0.4.22](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.21...plugin-rsc@0.4.22) (2025-08-22)
+### Bug Fixes
+
+* **rsc:** ensure `.js` suffix for internal virtual modules ([#744](https://github.com/vitejs/vite-plugin-react/issues/744)) ([bffc82e](https://github.com/vitejs/vite-plugin-react/commit/bffc82e12c3e8f369442eb4616db934d4bb10916))
+* **rsc:** expose only `"use server"` as server functions ([#752](https://github.com/vitejs/vite-plugin-react/issues/752)) ([d2f2e71](https://github.com/vitejs/vite-plugin-react/commit/d2f2e716773a0c95c32f5e65f0a0d7b016fc3250))
+* **rsc:** handle added/removed `"use client"` during dev ([#750](https://github.com/vitejs/vite-plugin-react/issues/750)) ([232be7b](https://github.com/vitejs/vite-plugin-react/commit/232be7bd65c7b0db8f6ecd41db6a97f47a9a9c26))
+* **rsc:** include non-entry optimized modules for `optimizeDeps.exclude` suggestion ([#740](https://github.com/vitejs/vite-plugin-react/issues/740)) ([2640add](https://github.com/vitejs/vite-plugin-react/commit/2640add3bfc9d0709de590b76599da59a131e506))
+* **rsc:** inject `__vite_rsc_importer_resources` import only once ([#742](https://github.com/vitejs/vite-plugin-react/issues/742)) ([5b28ba5](https://github.com/vitejs/vite-plugin-react/commit/5b28ba540cdeba511d7699df7331dec844893fc1))
+* **rsc:** isolate plugin state per plugin instance ([#747](https://github.com/vitejs/vite-plugin-react/issues/747)) ([596c76b](https://github.com/vitejs/vite-plugin-react/commit/596c76bfb919b668694c3768cb1126f9dbf7f878))
+* **rsc:** relax async function requirement for `"use server"` module directive ([#754](https://github.com/vitejs/vite-plugin-react/issues/754)) ([08986dd](https://github.com/vitejs/vite-plugin-react/commit/08986dd4d23d8881ed9852837508d64d38ff2129))
+
+### Code Refactoring
+
+* **rsc:** handle added/removed `"use server"` during dev ([#753](https://github.com/vitejs/vite-plugin-react/issues/753)) ([7542e6f](https://github.com/vitejs/vite-plugin-react/commit/7542e6f3b99054d065a8dc213a6ed62e3edde531))
+* **rsc:** organize internal plugins ([#745](https://github.com/vitejs/vite-plugin-react/issues/745)) ([0a6cfdf](https://github.com/vitejs/vite-plugin-react/commit/0a6cfdf874b47cee511cf308b9dae08b123eac70))
+* **rsc:** organize plugin utils ([#755](https://github.com/vitejs/vite-plugin-react/issues/755)) ([53b3f48](https://github.com/vitejs/vite-plugin-react/commit/53b3f485f6e06a34ddd70f3b1ffe35f4bebab3b3))
+* **rsc:** remove `__fix_cloudflare` plugin ([#746](https://github.com/vitejs/vite-plugin-react/issues/746)) ([bec6c82](https://github.com/vitejs/vite-plugin-react/commit/bec6c829e84d9ed36330ce9a16b602c0d6b73cf1))
+* **rsc:** simplify plugin state for server reference ([#751](https://github.com/vitejs/vite-plugin-react/issues/751)) ([9988f54](https://github.com/vitejs/vite-plugin-react/commit/9988f5494dd49e18a51fab9017a487da4843e4b0))
+
+## [0.4.21](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.20...plugin-rsc@0.4.21) (2025-08-19)
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#729](https://github.com/vitejs/vite-plugin-react/issues/729)) ([ba0323c](https://github.com/vitejs/vite-plugin-react/commit/ba0323cfcd7343362e64f782c5aae02ed9ee3273))
+* **rsc:** exclude CSS imports with special queries from automatic injection ([#580](https://github.com/vitejs/vite-plugin-react/issues/580)) ([71bb49c](https://github.com/vitejs/vite-plugin-react/commit/71bb49c7fe5c8362426d59ee8a99ea660b631b66))
+* **rsc:** fix custom `root` ([#717](https://github.com/vitejs/vite-plugin-react/issues/717)) ([c7bc716](https://github.com/vitejs/vite-plugin-react/commit/c7bc716e54070a35263dad1a978635c48f6c1720))
+* **rsc:** keep `import.meta.glob` during scan build for rolldown-vite ([#721](https://github.com/vitejs/vite-plugin-react/issues/721)) ([74ec0e0](https://github.com/vitejs/vite-plugin-react/commit/74ec0e0e0e21355884b0aff26ca0919404cef3f2))
+
+### Documentation
+
+* **rsc:** improve plugin-rsc README organization and clarity ([#723](https://github.com/vitejs/vite-plugin-react/issues/723)) ([e6d7392](https://github.com/vitejs/vite-plugin-react/commit/e6d7392f4c2b052db6ba719217641099cfa8f817))
+
+### Miscellaneous Chores
+
+* remove vite-plugin-inspect dependency from examples ([#730](https://github.com/vitejs/vite-plugin-react/issues/730)) ([feb5553](https://github.com/vitejs/vite-plugin-react/commit/feb55537d036dcd6f9008cb13a9748ca5ef57925))
+* **rsc:** fix `examples/basic` on stackblitz ([#724](https://github.com/vitejs/vite-plugin-react/issues/724)) ([1abe044](https://github.com/vitejs/vite-plugin-react/commit/1abe044668a13d55ea5549c558f666baa6196f15))
+* **rsc:** rework ssg example ([#713](https://github.com/vitejs/vite-plugin-react/issues/713)) ([28e723b](https://github.com/vitejs/vite-plugin-react/commit/28e723b6ad38c3aa15d6defb83c0b8acb6748f66))
+* **rsc:** tweak React.cache example ([#725](https://github.com/vitejs/vite-plugin-react/issues/725)) ([cc1bcdf](https://github.com/vitejs/vite-plugin-react/commit/cc1bcdfce4323119d0d918f72226168abbfadb4f))
+* **rsc:** use named imports ([#727](https://github.com/vitejs/vite-plugin-react/issues/727)) ([ba25233](https://github.com/vitejs/vite-plugin-react/commit/ba25233b3afafa20916ad35e4c7f1d3ecda0d0da))
+
+### Tests
+
+* **rsc:** fix invalid code ([#722](https://github.com/vitejs/vite-plugin-react/issues/722)) ([a39d837](https://github.com/vitejs/vite-plugin-react/commit/a39d8375cd0da1bd1e608894124bc7bfbffe6fa9))
+* **rsc:** test assets ([#733](https://github.com/vitejs/vite-plugin-react/issues/733)) ([fd96308](https://github.com/vitejs/vite-plugin-react/commit/fd96308a6cde57a132b3d9e434e711aac15c6486))
+
+## [0.4.20](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.19...plugin-rsc@0.4.20) (2025-08-13)
+### Bug Fixes
+
+* **rsc:** deprecate opt-out `ignoredPackageWarnings` option in favor of ont-in `DEBUG` env ([#697](https://github.com/vitejs/vite-plugin-react/issues/697)) ([5d5edd4](https://github.com/vitejs/vite-plugin-react/commit/5d5edd4d896fe6d064dddd5a3cf76594b0b0171c))
+* **rsc:** keep hoisted require order ([#706](https://github.com/vitejs/vite-plugin-react/issues/706)) ([ad7584a](https://github.com/vitejs/vite-plugin-react/commit/ad7584a29b02238d685504ff356515e6f78275dc))
+* **rsc:** remove duplicate server css on initial render ([#702](https://github.com/vitejs/vite-plugin-react/issues/702)) ([3114e88](https://github.com/vitejs/vite-plugin-react/commit/3114e88bcd8303d7c42da29eb7215c54ed43ce0d))
+* **rsc:** warn dual module of optimized and non-optimized client reference ([#705](https://github.com/vitejs/vite-plugin-react/issues/705)) ([e5c3517](https://github.com/vitejs/vite-plugin-react/commit/e5c351776e9a6269a37a171c830a902381af8011))
+
+### Miscellaneous Chores
+
+* **rsc:** fix csp example for Vite server ping SharedWorker ([#704](https://github.com/vitejs/vite-plugin-react/issues/704)) ([5b73cbe](https://github.com/vitejs/vite-plugin-react/commit/5b73cbe134466650a7aabc02dc794e7d6e35b135))
+* **rsc:** update package.json for starter-cf-single ([#707](https://github.com/vitejs/vite-plugin-react/issues/707)) ([2d93ee4](https://github.com/vitejs/vite-plugin-react/commit/2d93ee42cf8b4b544fd09400f1c6ed1dfdb6652d))
+
+### Code Refactoring
+
+* move @vitejs/plugin-rsc to devDependencies in examples ([#699](https://github.com/vitejs/vite-plugin-react/issues/699)) ([a1f4311](https://github.com/vitejs/vite-plugin-react/commit/a1f4311f87d0f983b8332ab393514e0d71263374))
+
+## [0.4.19](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.18...plugin-rsc@0.4.19) (2025-08-11)
+### Bug Fixes
+
+* **rsc:** fix cjs default import on module runner ([#695](https://github.com/vitejs/vite-plugin-react/issues/695)) ([c329914](https://github.com/vitejs/vite-plugin-react/commit/c329914c572473d4f09261fa0eba77484e720d2e))
+* **rsc:** replace `?v=` check with more robust `node_modules` detection ([#696](https://github.com/vitejs/vite-plugin-react/issues/696)) ([f0359c4](https://github.com/vitejs/vite-plugin-react/commit/f0359c4eca48ca6eb2ba98254a272949a13f149e))
+* **rsc:** replace non-optimized server cjs warning with debug only log ([#698](https://github.com/vitejs/vite-plugin-react/issues/698)) ([a88fb2d](https://github.com/vitejs/vite-plugin-react/commit/a88fb2ded4c8b9f42f2fee70a482615f331122f4))
+
+## [0.4.18](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.17...plugin-rsc@0.4.18) (2025-08-11)
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#694](https://github.com/vitejs/vite-plugin-react/issues/694)) ([5057858](https://github.com/vitejs/vite-plugin-react/commit/50578587472d23125980a46ff993fedaabca28d2))
+* **react:** always skip react-compiler on non client envrionment ([#689](https://github.com/vitejs/vite-plugin-react/issues/689)) ([2f62dc0](https://github.com/vitejs/vite-plugin-react/commit/2f62dc0778e8c527c7951d6e35b0658a07f1e6fc))
+* **rsc:** support cjs on module runner ([#687](https://github.com/vitejs/vite-plugin-react/issues/687)) ([7a92083](https://github.com/vitejs/vite-plugin-react/commit/7a92083eadb6ad8d92e6e560de414bc600e977c0))
+
+### Miscellaneous Chores
+
+* **rsc:** add .gitignore to create-vite example ([#686](https://github.com/vitejs/vite-plugin-react/issues/686)) ([6df7192](https://github.com/vitejs/vite-plugin-react/commit/6df71929ea5c2176408054bc40bcb8dfbb370018))
+* **rsc:** mention deploy example ([#685](https://github.com/vitejs/vite-plugin-react/issues/685)) ([dea484a](https://github.com/vitejs/vite-plugin-react/commit/dea484ab8c740babab89da0f716bb929e57ba2af))
+
+## [0.4.17](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.16...plugin-rsc@0.4.17) (2025-08-05)
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#670](https://github.com/vitejs/vite-plugin-react/issues/670)) ([61d777d](https://github.com/vitejs/vite-plugin-react/commit/61d777ddc8524256f890f43a2a78dbfbfd1e97ac))
+* **rsc:** keep manually added link stylesheet during dev ([#663](https://github.com/vitejs/vite-plugin-react/issues/663)) ([ac20b31](https://github.com/vitejs/vite-plugin-react/commit/ac20b31279f6884169503ef6e5786639c93251df))
+* **rsc:** optimize `use-sync-external-store` ([#674](https://github.com/vitejs/vite-plugin-react/issues/674)) ([556de15](https://github.com/vitejs/vite-plugin-react/commit/556de15191eb2dfa26d9c0ba396c219d4b4a2dd4))
+
+### Documentation
+
+* **rsc:** notes on CSS support ([#673](https://github.com/vitejs/vite-plugin-react/issues/673)) ([9b2741f](https://github.com/vitejs/vite-plugin-react/commit/9b2741f3dc3da8e9e2ef486ab8d7eaa317230f7d))
+
+### Miscellaneous Chores
+
+* **rsc:** tweak types and examples ([#682](https://github.com/vitejs/vite-plugin-react/issues/682)) ([7b07098](https://github.com/vitejs/vite-plugin-react/commit/7b07098746a672950f278ea7edffd04834133d1f))
+
+### Code Refactoring
+
+* **rsc:** update `@mjackson/node-fetch-server` to `@remix-run/node-fetch-server` ([#680](https://github.com/vitejs/vite-plugin-react/issues/680)) ([97b5f1b](https://github.com/vitejs/vite-plugin-react/commit/97b5f1b26c2260825447c7e9781f1b168bebbe62))
+
+### Tests
+
+* **rsc:** test `React.cache` ([#668](https://github.com/vitejs/vite-plugin-react/issues/668)) ([26ad4ad](https://github.com/vitejs/vite-plugin-react/commit/26ad4adcb69affb8932151f245b25a8fcf95c85a))
+* **rsc:** test shared module hmr ([#671](https://github.com/vitejs/vite-plugin-react/issues/671)) ([775ac61](https://github.com/vitejs/vite-plugin-react/commit/775ac6157ef7af545b4cb03ff116a01c7cffa815))
+
+## [0.4.16](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.15...plugin-rsc@0.4.16) (2025-08-01)
+### Features
+
+* merge `plugin-react-oxc` into `plugin-react` ([#609](https://github.com/vitejs/vite-plugin-react/issues/609)) ([133d786](https://github.com/vitejs/vite-plugin-react/commit/133d7865f42aa3376b5d3119fdb6a71eaf600275))
+* **rsc:** add `useBuildAppHook` option to switch `plugin.buildApp` or `builder.buildApp` ([#653](https://github.com/vitejs/vite-plugin-react/issues/653)) ([83a5741](https://github.com/vitejs/vite-plugin-react/commit/83a57414169684bc705a5f6ca13cf097225117d8))
+* **rsc:** support `client` environment as `react-server` ([#657](https://github.com/vitejs/vite-plugin-react/issues/657)) ([5df0070](https://github.com/vitejs/vite-plugin-react/commit/5df00707522ecbcda40f2c53c620f46b517e68e6))
+
+### Bug Fixes
+
+* **react:** use development jsx transform for `NODE_ENV=development` build ([#649](https://github.com/vitejs/vite-plugin-react/issues/649)) ([9ffd86d](https://github.com/vitejs/vite-plugin-react/commit/9ffd86df3c0cfc2060669cac7cc0b86144158b1b))
+* **rsc:** avoid unnecessary server hmr due to tailwind module deps ([#658](https://github.com/vitejs/vite-plugin-react/issues/658)) ([c1383f8](https://github.com/vitejs/vite-plugin-react/commit/c1383f870137c0f152d7687250e8095635a1177c))
+
+### Miscellaneous Chores
+
+* **deps:** update all non-major dependencies ([#639](https://github.com/vitejs/vite-plugin-react/issues/639)) ([1a02ba7](https://github.com/vitejs/vite-plugin-react/commit/1a02ba7f4d3fe4a1696b43bc5161d6d466802faf))
+
+### Code Refactoring
+
+* **rsc:** move `writeManifest` inside `buildApp` hook ([#659](https://github.com/vitejs/vite-plugin-react/issues/659)) ([a34f8c5](https://github.com/vitejs/vite-plugin-react/commit/a34f8c537df2efc27d55a510bfd3597c639842f6))
+* **rsc:** split encryption runtime exports ([#660](https://github.com/vitejs/vite-plugin-react/issues/660)) ([ff44ae4](https://github.com/vitejs/vite-plugin-react/commit/ff44ae49697e6ebca4ae4b241ab8337ebe659b5e))
+
+### Tests
+
+* **rsc:** port transform tests from waku ([#655](https://github.com/vitejs/vite-plugin-react/issues/655)) ([c602225](https://github.com/vitejs/vite-plugin-react/commit/c602225271d4acf462ba00f8d6d8a2e42492c5cd))
+* **rsc:** split more independent tests ([#652](https://github.com/vitejs/vite-plugin-react/issues/652)) ([ac0cac7](https://github.com/vitejs/vite-plugin-react/commit/ac0cac7465cc94e91e8ac40269f36e91599b8162))
+
+## [0.4.15](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.14...plugin-rsc@0.4.15) (2025-07-28)
+### Features
+
+* **rsc:** show warning for non optimized cjs ([#635](https://github.com/vitejs/vite-plugin-react/issues/635)) ([da0a786](https://github.com/vitejs/vite-plugin-react/commit/da0a78607d18be534232fba5ea95bb96cc987449))
+
+### Bug Fixes
+
+* **rsc:** improve auto css heuristics ([#643](https://github.com/vitejs/vite-plugin-react/issues/643)) ([f0b4cff](https://github.com/vitejs/vite-plugin-react/commit/f0b4cff636558a27ed4e5527ed4ea68a2243e40e))
+
+## [0.4.14](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.13...plugin-rsc@0.4.14) (2025-07-27)
+### Features
+
+* **rsc:** validate `client-only` and `server-only` import during resolve ([#624](https://github.com/vitejs/vite-plugin-react/issues/624)) ([47d02d0](https://github.com/vitejs/vite-plugin-react/commit/47d02d0643cecc8243c72fddd9e125cc3d020847))
+
+### Bug Fixes
+
+* **rsc:** add `getEntrySource` assertion error message ([#633](https://github.com/vitejs/vite-plugin-react/issues/633)) ([4568556](https://github.com/vitejs/vite-plugin-react/commit/45685561d7e85cd6e2f77dc383cc6728d5fc916f))
+* **rsc:** handle transform errors before server hmr ([#626](https://github.com/vitejs/vite-plugin-react/issues/626)) ([d28356f](https://github.com/vitejs/vite-plugin-react/commit/d28356f5caca2867ced9af3a02a3f441ff4a5238))
+
+### Documentation
+
+* **rsc:** fix jsdoc ([#623](https://github.com/vitejs/vite-plugin-react/issues/623)) ([73d457b](https://github.com/vitejs/vite-plugin-react/commit/73d457b2774c26a9fd1ec0f53aee8b4ff60dacd6))
+
+### Miscellaneous Chores
+
+* **deps:** update react-router ([#632](https://github.com/vitejs/vite-plugin-react/issues/632)) ([b077c4a](https://github.com/vitejs/vite-plugin-react/commit/b077c4a774ebe4a059902f3e0cb043c7194cceeb))
+
+### Tests
+
+* **rsc:** parallel e2e ([#628](https://github.com/vitejs/vite-plugin-react/issues/628)) ([24ddea4](https://github.com/vitejs/vite-plugin-react/commit/24ddea46d016311a8efe34314a4faa9d61af0d9d))
+* **rsc:** split starter tests into multiple files ([#629](https://github.com/vitejs/vite-plugin-react/issues/629)) ([707f35b](https://github.com/vitejs/vite-plugin-react/commit/707f35bfe1fb047a453fca6281885bc1565303fc))
+
+### Continuous Integration
+
+* **rsc:** test react nightly ([#630](https://github.com/vitejs/vite-plugin-react/issues/630)) ([3e2f5a9](https://github.com/vitejs/vite-plugin-react/commit/3e2f5a9e03f56d1a218f030a71be72ef28b91a43))
+
+## [0.4.13](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.12...plugin-rsc@0.4.13) (2025-07-24)
+### Features
+
+* **rsc:** add support for `experimental.renderBuiltUrl` on assets metadata ([#612](https://github.com/vitejs/vite-plugin-react/issues/612)) ([5314ed6](https://github.com/vitejs/vite-plugin-react/commit/5314ed60572e2c89963e5a720d21bcad17687382))
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#568](https://github.com/vitejs/vite-plugin-react/issues/568)) ([d14f31d](https://github.com/vitejs/vite-plugin-react/commit/d14f31d3bf8487346ae6f9db7e6ca7263c93066b))
+* **deps:** update all non-major dependencies ([#593](https://github.com/vitejs/vite-plugin-react/issues/593)) ([9ce3b22](https://github.com/vitejs/vite-plugin-react/commit/9ce3b22e4bc7db28f549b9c9b9195d2bd82ff736))
+* **rsc:** await handler to avoid unhandled rejection ([#576](https://github.com/vitejs/vite-plugin-react/issues/576)) ([fa60127](https://github.com/vitejs/vite-plugin-react/commit/fa60127be46d48ecd8a8b0d0e7e6751ed11303e2))
+* **rsc:** ensure trailing slash of `BASE_URL` ([#589](https://github.com/vitejs/vite-plugin-react/issues/589)) ([fa1d260](https://github.com/vitejs/vite-plugin-react/commit/fa1d260ef384d986284aaec6e0984967f3b436ad))
+* **rsc:** update rsc-html-stream v0.0.7 ([#578](https://github.com/vitejs/vite-plugin-react/issues/578)) ([df6a38e](https://github.com/vitejs/vite-plugin-react/commit/df6a38e42339cf5deecd3f1b6c0aa4dd838833c5))
+
+### Documentation
+
+* **rsc:** add `CONTRIBUTING.md` ([#613](https://github.com/vitejs/vite-plugin-react/issues/613)) ([4005dbe](https://github.com/vitejs/vite-plugin-react/commit/4005dbe1bb943b882d8199ef29ccaeb9d268784e))
+
+### Miscellaneous Chores
+
+* replace `build --app` with `build` in examples ([#572](https://github.com/vitejs/vite-plugin-react/issues/572)) ([7c564ff](https://github.com/vitejs/vite-plugin-react/commit/7c564ff4f290a554927f2eef600e82bffee16e6b))
+* **rsc:** comment ([#599](https://github.com/vitejs/vite-plugin-react/issues/599)) ([b550b63](https://github.com/vitejs/vite-plugin-react/commit/b550b63fe7f6ef82588ff0d60389d11906c3cc4e))
+* **rsc:** deprecate `@vitejs/plugin-rsc/extra` API ([#592](https://github.com/vitejs/vite-plugin-react/issues/592)) ([bd6a2a1](https://github.com/vitejs/vite-plugin-react/commit/bd6a2a1ff272c8550f92bc1530c7b28fb81e1c60))
+* **rsc:** deprecate `rsc-html-stream` re-exports ([#602](https://github.com/vitejs/vite-plugin-react/issues/602)) ([8e0e8b6](https://github.com/vitejs/vite-plugin-react/commit/8e0e8b60c511f34df188a8e8b103cf273891d7ad))
+* **rsc:** fix temporary references in examples ([#603](https://github.com/vitejs/vite-plugin-react/issues/603)) ([22e5398](https://github.com/vitejs/vite-plugin-react/commit/22e53987a5548d237fcbe61377bd1da6e86947ef))
+* **rsc:** move comment ([#604](https://github.com/vitejs/vite-plugin-react/issues/604)) ([4d6c72f](https://github.com/vitejs/vite-plugin-react/commit/4d6c72f81d64972ac84735240d27516be81431f8))
+* **rsc:** remove `@vite/plugin-rsc/extra` API usages from examples ([#596](https://github.com/vitejs/vite-plugin-react/issues/596)) ([87319bf](https://github.com/vitejs/vite-plugin-react/commit/87319bf94ddb07061a1a80d3eefbfadb980f7008))
+* **rsc:** remove console.log ([#607](https://github.com/vitejs/vite-plugin-react/issues/607)) ([2a7ff5c](https://github.com/vitejs/vite-plugin-react/commit/2a7ff5c93e600b06aafc7ce1a6d8a11c2ad4cf2e))
+* **rsc:** tweak changelog ([#570](https://github.com/vitejs/vite-plugin-react/issues/570)) ([8804446](https://github.com/vitejs/vite-plugin-react/commit/88044469a6399c8a1d909b564f6ddc039782c066))
+* **rsc:** update React Router RSC references ([#581](https://github.com/vitejs/vite-plugin-react/issues/581)) ([d464e8f](https://github.com/vitejs/vite-plugin-react/commit/d464e8fc9e8e14bdc84051de9ffacec16317d2ae))
+
+### Tests
+
+* **rsc:** add more basic tests to starter ([#600](https://github.com/vitejs/vite-plugin-react/issues/600)) ([d7fcdd8](https://github.com/vitejs/vite-plugin-react/commit/d7fcdd8550a7a11da01887cbf48a646af898b7f1))
+* **rsc:** add SSR thenable workaround in examples ([#591](https://github.com/vitejs/vite-plugin-react/issues/591)) ([bfd434f](https://github.com/vitejs/vite-plugin-react/commit/bfd434f7fdd063ad017aa3c3a41e42983efc0ef4))
+* **rsc:** add transitive cjs dep example ([#611](https://github.com/vitejs/vite-plugin-react/issues/611)) ([2a81b90](https://github.com/vitejs/vite-plugin-react/commit/2a81b9015286558c1463ab8079a7a6e40a82a5c6))
+* **rsc:** refactor variant tests ([#601](https://github.com/vitejs/vite-plugin-react/issues/601)) ([5167266](https://github.com/vitejs/vite-plugin-react/commit/5167266aff6671065cf5b49cf8ada3d0ace2bbb4))
+* **rsc:** remove global unhandled error handlers ([#597](https://github.com/vitejs/vite-plugin-react/issues/597)) ([c5f0bab](https://github.com/vitejs/vite-plugin-react/commit/c5f0babdc06c813bbef08d3c44ee696789416116))
+* **rsc:** support `fs:cp` command in `setupInlineFixture` ([#621](https://github.com/vitejs/vite-plugin-react/issues/621)) ([d9cb926](https://github.com/vitejs/vite-plugin-react/commit/d9cb92650b217abba4144d62737c5c696b55d0bb))
+* **rsc:** test build with `NODE_ENV=development` and vice versa ([#606](https://github.com/vitejs/vite-plugin-react/issues/606)) ([e8fa2d0](https://github.com/vitejs/vite-plugin-react/commit/e8fa2d0b4cb6e1dd3132fe8b7f45529a74d9be03))
+* **rsc:** test module runner `hmr: false` ([#595](https://github.com/vitejs/vite-plugin-react/issues/595)) ([7223093](https://github.com/vitejs/vite-plugin-react/commit/7223093d793242f3d1ef313bbfec692499f0659e))
+
+## [0.4.12](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.11...plugin-rsc@0.4.12) (2025-07-14)
+### Features
+
+* **rsc:** support regex directive for `transformHoistInlineDirective` ([#527](https://github.com/vitejs/vite-plugin-react/issues/527)) ([b598bb5](https://github.com/vitejs/vite-plugin-react/commit/b598bb57d6a7d76bb4ce41ae5990913461949ec3))
+
+### Bug Fixes
+
+* **rsc:** support setups without an SSR environment ([#562](https://github.com/vitejs/vite-plugin-react/issues/562)) ([0fc7fcd](https://github.com/vitejs/vite-plugin-react/commit/0fc7fcdae31568dcd2568a10333ad1e79e2d5176))
+
+## [0.4.11](https://github.com/vitejs/vite-plugin-react/compare/plugin-rsc@0.4.10...plugin-rsc@0.4.11) (2025-07-07)
+### Miscellaneous Chores
+
+* fix rsc release ([#543](https://github.com/vitejs/vite-plugin-react/issues/543)) ([58c8bfd](https://github.com/vitejs/vite-plugin-react/commit/58c8bfd1f4e9584d81cb5e85aa466119fd72bbbc))
+
+## 0.4.10 (2025-07-07)
+### Features
+
+* add `@vitejs/plugin-rsc` ([#521](https://github.com/vitejs/vite-plugin-react/issues/521)) ([0318334](https://github.com/vitejs/vite-plugin-react/commit/03183346630c73fa58ca4d403785a36913535bb6))
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#540](https://github.com/vitejs/vite-plugin-react/issues/540)) ([cfe2912](https://github.com/vitejs/vite-plugin-react/commit/cfe29122a8eec6c1e2ed9999531237dbce140e60))
+* return `Plugin[]` instead of `PluginOption[]` ([#537](https://github.com/vitejs/vite-plugin-react/issues/537)) ([11f56d6](https://github.com/vitejs/vite-plugin-react/commit/11f56d63a9ed082137732211db556c784cadb523))
+
+## v0.4.10-alpha.1 (2025-07-04)
+
+- feat: add `@vitejs/plugin-rsc` ([#521](https://github.com/vitejs/vite-plugin-react/pull/521))
+
+---
+
+Older versions were released as [`@hi-ogawa/vite-rsc`](https://www.npmjs.com/package/@hiogawa/vite-rsc).
+
+## v0.4.9 (2025-07-03)
+
+- feat: re-export plugin from base exports entry ([#1125](https://github.com/hi-ogawa/vite-plugins/pull/1125))
+- feat: re-export `transformHoistInlineDirective` ([#1122](https://github.com/hi-ogawa/vite-plugins/pull/1122))
+- fix: don't copy vite manifest from rsc to client ([#1118](https://github.com/hi-ogawa/vite-plugins/pull/1118))
+
+## v0.4.8 (2025-07-01)
+
+- fix: copy all server assets to client by default and output `__vite_rsc_encryption_key` to fs directly ([#1102](https://github.com/hi-ogawa/vite-plugins/pull/1102))
+- fix: stable client build ([#1094](https://github.com/hi-ogawa/vite-plugins/pull/1094))
+
+## v0.4.7 (2025-06-28)
+
+- feat: re-export `encodeReply` and `createTemporaryReferenceSet` from `react-server-dom/client` in `rsc` ([#1089](https://github.com/hi-ogawa/vite-plugins/pull/1089))
+- chore: add `use cache` example ([#1089](https://github.com/hi-ogawa/vite-plugins/pull/1089))
+- refactor: output code without indent ([#1087](https://github.com/hi-ogawa/vite-plugins/pull/1087))
+
+## v0.4.6 (2025-06-27)
+
+- fix: correctly resolve server function created by 3rd party package during dev ([#1067](https://github.com/hi-ogawa/vite-plugins/pull/1067))
+- fix: correctly resolve client boundary created by server package during dev ([#1050](https://github.com/hi-ogawa/vite-plugins/pull/1050))
+- fix: copy only css assets from server build to client build by default ([#1072](https://github.com/hi-ogawa/vite-plugins/pull/1072))
+- fix: fix single quote string in `loadModule('ssr', 'index')` ([#1064](https://github.com/hi-ogawa/vite-plugins/pull/1064))
+- fix: stabilize server build by externalizing encryption key file ([#1069](https://github.com/hi-ogawa/vite-plugins/pull/1069))
+- fix: check build instead of `import.meta.env.DEV` ([#1083](https://github.com/hi-ogawa/vite-plugins/pull/1083))
+- perf: strip code during scan build ([#1066](https://github.com/hi-ogawa/vite-plugins/pull/1066))
+- feat: support preserving client reference original value ([#1078](https://github.com/hi-ogawa/vite-plugins/pull/1078))
+- feat: add `enableActionEncryption` option for debugging purpose ([#1084](https://github.com/hi-ogawa/vite-plugins/pull/1084))
+- feat: add `ignoredClientInServerPackageWarning` option ([#1065](https://github.com/hi-ogawa/vite-plugins/pull/1065))
+
+## v0.4.5 (2025-06-22)
+
+- feat: rsc css transform for default export identifier ([#1046](https://github.com/hi-ogawa/vite-plugins/pull/1046))
+- feat: add `import.meta.viteRsc.loadBootstrapScriptContent` ([#1042](https://github.com/hi-ogawa/vite-plugins/pull/1042))
+- fix: only include jsx/tsx for rsc css export transform ([#1034](https://github.com/hi-ogawa/vite-plugins/pull/1034))
+- fix: ensure server-only and client-only not externalized ([#1045](https://github.com/hi-ogawa/vite-plugins/pull/1045))
+- fix: use static import for `loadCss` virtuals during build ([#1043](https://github.com/hi-ogawa/vite-plugins/pull/1043))
+
+## v0.4.4 (2025-06-20)
+
+- feat: automatic rsc css export transform ([#1030](https://github.com/hi-ogawa/vite-plugins/pull/1030))
+- feat: add plugin to workaround cloudflare error ([#1014](https://github.com/hi-ogawa/vite-plugins/pull/1014))
+- feat: add load module dev proxy ([#1012](https://github.com/hi-ogawa/vite-plugins/pull/1012))
+- feat: add `serverHandler` option to allow using ssr environment as main handler ([#1008](https://github.com/hi-ogawa/vite-plugins/pull/1008))
+- feat: support `loadModule(environment, entry)` ([#1007](https://github.com/hi-ogawa/vite-plugins/pull/1007))
+- refactor: tweak renderHtml types and naming ([#1029](https://github.com/hi-ogawa/vite-plugins/pull/1029))
+
+## v0.4.3 (2025-06-18)
+
+- feat: add rsc css export transform helper ([#1002](https://github.com/hi-ogawa/vite-plugins/pull/1002))
+- feat: support `loadCss(importer)` ([#1001](https://github.com/hi-ogawa/vite-plugins/pull/1001))
+
+## v0.4.2 (2025-06-17)
+
+- fix: allow custom `outDir` + chore: cloudflare single worker setup ([#990](https://github.com/hi-ogawa/vite-plugins/pull/990))
+- fix: transform `__webpack_require__` global ([#980](https://github.com/hi-ogawa/vite-plugins/pull/980))
+- fix: inline and optimize react deps in ssr environment ([#982](https://github.com/hi-ogawa/vite-plugins/pull/982))
+- refactor: resolve self runtime import instead of `dedupe` ([#975](https://github.com/hi-ogawa/vite-plugins/pull/975))
+- refactor: emit assets manifest during `writeBundle` ([#972](https://github.com/hi-ogawa/vite-plugins/pull/972))
+- refactor: use `../` instead of `./../` path in output ([#963](https://github.com/hi-ogawa/vite-plugins/pull/963))
+
+## v0.4.1 (2025-06-15)
+
+- fix: re-publish to fix vendored dependency
+
+## v0.4.0 (2025-06-15)
+
+- refactor!: rework multi environment API (bootstrap script) ([#958](https://github.com/hi-ogawa/vite-plugins/pull/958))
+- refactor!: rework multi environment API (ssr module) ([#957](https://github.com/hi-ogawa/vite-plugins/pull/957))
+- refactor!: simplify plugin options in favor of `rollupOptions.input` ([#956](https://github.com/hi-ogawa/vite-plugins/pull/956))
+- feat: expose `rsc-html-stream` utils ([#950](https://github.com/hi-ogawa/vite-plugins/pull/950))
+- fix: fix missing rsc css on build ([#949](https://github.com/hi-ogawa/vite-plugins/pull/949))
+
+## v0.3.4 (2025-06-12)
+
+- fix: fix internal import to allow stable react vendor chunk ([#824](https://github.com/hi-ogawa/vite-plugins/pull/824))
+- fix: compat for old react plugin ([#939](https://github.com/hi-ogawa/vite-plugins/pull/939))
+
+## v0.3.3 (2025-06-12)
+
+- feat: support rolldown-vite ([#931](https://github.com/hi-ogawa/vite-plugins/pull/931))
+- fix: allow usage without react plugin ([#934](https://github.com/hi-ogawa/vite-plugins/pull/934))
+- chore: docs ([#921](https://github.com/hi-ogawa/vite-plugins/pull/921))
+
+## v0.3.2 (2025-06-10)
+
+- feat: auto initialize ([#925](https://github.com/hi-ogawa/vite-plugins/pull/925))
+- fix: emit assets manifest only in server build ([#929](https://github.com/hi-ogawa/vite-plugins/pull/929))
+- refactor: inline react-server-dom in ssr (2) ([#927](https://github.com/hi-ogawa/vite-plugins/pull/927))
+- chore: add `@cloudflare/vite-plugin` example ([#926](https://github.com/hi-ogawa/vite-plugins/pull/926))
+
+## v0.3.1 (2025-06-06)
+
+- refactor: vendor react-server-dom ([#854](https://github.com/hi-ogawa/vite-plugins/pull/854))
+
+## v0.3.0 (2025-06-05)
+
+- feat!: rsc css code split ([#876](https://github.com/hi-ogawa/vite-plugins/pull/876))
+- feat: encrypt closure bind values ([#897](https://github.com/hi-ogawa/vite-plugins/pull/897))
+- fix: client element as bound arg encryption ([#905](https://github.com/hi-ogawa/vite-plugins/pull/905))
+- fix: throw on client reference call on server ([#900](https://github.com/hi-ogawa/vite-plugins/pull/900))
+
+## v0.2.4 (2025-05-26)
+
+- fix: fix stale css import in non-boundary client module ([#887](https://github.com/hi-ogawa/vite-plugins/pull/887))
+- fix: fix non-client-boundary client module hmr in tailwind example ([#886](https://github.com/hi-ogawa/vite-plugins/pull/886))
+
+## v0.2.3 (2025-05-22)
+
+- fix: support Windows ([#884](https://github.com/hi-ogawa/vite-plugins/pull/884))
+- fix: remove stale ssr styles during dev ([#879](https://github.com/hi-ogawa/vite-plugins/pull/879))
+- fix: add `vary` header to avoid rsc payload on tab re-open ([#877](https://github.com/hi-ogawa/vite-plugins/pull/877))
+
+## v0.2.2 (2025-05-18)
+
+- fix: emit server assets and copy to client ([#861](https://github.com/hi-ogawa/vite-plugins/pull/861))
+- fix: css modules hmr ([#860](https://github.com/hi-ogawa/vite-plugins/pull/860))
+- fix: fix `collectCssByUrl` error ([#856](https://github.com/hi-ogawa/vite-plugins/pull/856))
+- fix: show invalid transform error with code frame ([#871](https://github.com/hi-ogawa/vite-plugins/pull/871))
+- perf: preload client reference deps before non-cached import ([#850](https://github.com/hi-ogawa/vite-plugins/pull/850))
+
+## v0.2.1 (2025-05-13)
+
+- feat: automatic client package heuristics ([#830](https://github.com/hi-ogawa/vite-plugins/pull/830))
+- fix: add browser entry to `optimizeDeps.entries` ([#846](https://github.com/hi-ogawa/vite-plugins/pull/846))
+- fix: resolve self package from project root ([#845](https://github.com/hi-ogawa/vite-plugins/pull/845))
+- refactor: use `rsc-html-stream` ([#843](https://github.com/hi-ogawa/vite-plugins/pull/843))
+
+## v0.2.0 (2025-05-12)
+
+- feat: apply tree-shaking to all client references (2nd approach) ([#838](https://github.com/hi-ogawa/vite-plugins/pull/838))
+- feat: support nonce ([#813](https://github.com/hi-ogawa/vite-plugins/pull/813))
+- feat: support css in rsc environment ([#825](https://github.com/hi-ogawa/vite-plugins/pull/825))
+- feat: support css in client references ([#823](https://github.com/hi-ogawa/vite-plugins/pull/823))
+- fix: handle html escape and binary data in ssr rsc payload ([#839](https://github.com/hi-ogawa/vite-plugins/pull/839))
+- fix: wrap virtual to workaround module runner entry issues ([#832](https://github.com/hi-ogawa/vite-plugins/pull/832))
+- fix: scan build in two environments ([#820](https://github.com/hi-ogawa/vite-plugins/pull/820))
+- refactor: simplify client reference mapping ([#836](https://github.com/hi-ogawa/vite-plugins/pull/836))
+- refactor!: remove `entries.css` ([#831](https://github.com/hi-ogawa/vite-plugins/pull/831))
+- refactor: client reference ssr preinit/preload via proxy and remove `prepareDestination` ([#828](https://github.com/hi-ogawa/vite-plugins/pull/828))
+- refactor: tweak asset links api ([#826](https://github.com/hi-ogawa/vite-plugins/pull/826))
+
+## v0.1.1 (2025-05-07)
+
+- fix: statically import client references virtual ([#815](https://github.com/hi-ogawa/vite-plugins/pull/815))
+- fix: fix base for findSourceMapURL ([#812](https://github.com/hi-ogawa/vite-plugins/pull/812))
+- fix: fix module runner line offset in `findSourceMapURL` ([#810](https://github.com/hi-ogawa/vite-plugins/pull/810))
+
+## v0.1.0 (2025-05-01)
+
+- feat: support `findSourceMapURL` for `createServerReference` ([#796](https://github.com/hi-ogawa/vite-plugins/pull/796))
+- feat: support `findSourceMapURL` for component stack and replay logs ([#779](https://github.com/hi-ogawa/vite-plugins/pull/779))
+- feat: support temporary references ([#776](https://github.com/hi-ogawa/vite-plugins/pull/776))
+- feat: support custom base ([#775](https://github.com/hi-ogawa/vite-plugins/pull/775))
+- feat: refactor assets manifest and expose it to rsc build ([#767](https://github.com/hi-ogawa/vite-plugins/pull/767))
+- feat: ssr modulepreload only for build ([#763](https://github.com/hi-ogawa/vite-plugins/pull/763))
+- feat: tree shake unused reference exports ([#761](https://github.com/hi-ogawa/vite-plugins/pull/761))
+- feat: re-export react-server-dom ([#744](https://github.com/hi-ogawa/vite-plugins/pull/744))
+- feat: support css entry ([#737](https://github.com/hi-ogawa/vite-plugins/pull/737))
+- feat wrap client packages in virtual (support `clientPackages` options) ([#718](https://github.com/hi-ogawa/vite-plugins/pull/718))
+- feat: modulepreload client reference on ssr ([#703](https://github.com/hi-ogawa/vite-plugins/pull/703))
+- feat: create vite-rsc ([#692](https://github.com/hi-ogawa/vite-plugins/pull/692))
diff --git a/packages/plugin-rsc/CONTRIBUTING.md b/packages/plugin-rsc/CONTRIBUTING.md
new file mode 100644
index 000000000..7de848fb2
--- /dev/null
+++ b/packages/plugin-rsc/CONTRIBUTING.md
@@ -0,0 +1,66 @@
+# Contributing to @vitejs/plugin-rsc
+
+This guide provides essential tips for contributors working on the RSC plugin.
+
+## Testing
+
+### E2E Test Setup
+
+Tests use Playwright and are located in `e2e/` and use `examples` as test apps.
+
+#### Test Fixture Patterns
+
+- `examples/basic` - comprehensive test suite for the RSC plugin
+- `examples/starter` - lightweight base template for writing more targeted tests using `setupInlineFixture` utility
+- `examples/e2e/temp/` - base directory for test projects
+
+### Adding New Test Cases
+
+**Expanding `examples/basic` (for comprehensive features)**
+Best for features that should be part of the main test suite. `examples/basic` is mainly used for e2e testing:
+
+1. Add your test case files to `examples/basic/src/routes/`
+2. Update the routing in `examples/basic/src/routes/root.tsx`
+3. Add corresponding tests in `e2e/basic.test.ts`
+
+**Using `setupInlineFixture` (for specific edge cases)**
+Best for testing specific edge cases or isolated features. See `e2e/ssr-thenable.test.ts` for the pattern.
+
+
+
+## Development Workflow
+
+
+
+```bash
+# Build packages
+pnpm dev # pnpm -C packages/plugin-rsc dev
+
+# Type check
+pnpm -C packages/plugin-rsc tsc-dev
+
+# Run examples
+pnpm -C packages/plugin-rsc/examples/basic dev # build / preview
+pnpm -C packages/plugin-rsc/examples/starter dev # build / preview
+
+# Run all e2e tests
+pnpm -C packages/plugin-rsc test-e2e
+
+# Run with UI (this allows filtering interactively)
+pnpm -C packages/plugin-rsc test-e2e --ui
+
+# Run specific test file
+pnpm -C packages/plugin-rsc test-e2e basic
+
+# Run with filter/grep
+pnpm -C packages/plugin-rsc test-e2e -g "hmr"
+
+# Test projects created with `setupInlineFixture` are locally runnable. For example:
+pnpm -C packages/plugin-rsc/examples/e2e/temp/react-compiler dev
+```
+
+## Tips
+
+- Prefer `setupInlineFixture` for new tests - it's more maintainable and faster
+- The `examples/basic` project contains comprehensive test scenarios
+- Dependencies for temp test projects are managed in `examples/e2e/package.json`
diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md
new file mode 100644
index 000000000..dbddc01dc
--- /dev/null
+++ b/packages/plugin-rsc/README.md
@@ -0,0 +1,573 @@
+# @vitejs/plugin-rsc
+
+This package provides [React Server Components](https://react.dev/reference/rsc/server-components) (RSC) support for Vite.
+
+## Features
+
+- **Framework-agnostic**: The plugin implements [RSC bundler features](https://react.dev/reference/rsc/server-components) and provides low level RSC runtime (`react-server-dom`) API without framework-specific abstractions.
+- **Runtime-agnostic**: Built on [Vite environment API](https://vite.dev/guide/api-environment.html) and works with other runtimes (e.g., [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare)).
+- **HMR support**: Enables editing both client and server components without full page reloads.
+- **CSS support**: CSS is automatically code-split both at client and server components and they are injected upon rendering.
+
+## Getting Started
+
+You can create a starter project by:
+
+```sh
+npm create vite@latest -- --template rsc
+```
+
+## Examples
+
+**Start here:** [`./examples/starter`](./examples/starter) - Recommended for understanding the package
+
+- Provides an in-depth overview of API with inline comments to explain how they function within RSC-powered React application.
+
+**Integration examples:**
+
+- [`./examples/basic`](./examples/basic) - Advanced RSC features and testing
+ - This is mainly used for e2e testing and includes various advanced RSC usages (e.g. `"use cache"` example).
+- [`./examples/ssg`](./examples/ssg) - Static site generation with MDX and client components for interactivity.
+- [`./examples/react-router`](./examples/react-router) - React Router RSC integration
+ - Demonstrates how to integrate [experimental React Router RSC API](https://remix.run/blog/rsc-preview). React Router now provides [official RSC support](https://reactrouter.com/how-to/react-server-components), so it's recommended to follow React Router's official documentation for the latest integration.
+
+## Basic Concepts
+
+This example is a simplified version of [`./examples/starter`](./examples/starter). You can read [`./examples/starter/src/framework/entry.{rsc,ssr,browser}.tsx`](./examples/starter/src/framework) for more in-depth commentary, which includes server function handling and client-side RSC re-fetching/re-rendering.
+
+This is the diagram to show the basic flow of RSC rendering process. See also https://github.com/hi-ogawa/vite-plugins/discussions/606.
+
+```mermaid
+graph TD
+
+ subgraph "rsc environment"
+ A["React virtual dom tree"] --> |"[@vitejs/plugin-rsc/rsc] renderToReadableStream"| B1["RSC Stream"];
+ end
+
+ B1 --> B2
+ B1 --> B3
+
+ subgraph "ssr environment"
+ B2["RSC Stream"] --> |"[@vitejs/plugin-rsc/ssr] createFromReadableStream"| C1["React virtual dom tree"];
+ C1 --> |"[react-dom/server] SSR"| E["HTML String/Stream"];
+ end
+
+ subgraph "client environment"
+ B3["RSC Stream"] --> |"[@vitejs/plugin-rsc/browser] createFromReadableStream"| C2["React virtual dom tree"];
+ C2 --> |"[react-dom/client] CSR: mount, hydration"| D["DOM Elements"];
+ end
+
+ style A fill:#D6EAF8,stroke:#333,stroke-width:2px
+ style B1 fill:#FEF9E7,stroke:#333,stroke-width:2px
+ style B2 fill:#FEF9E7,stroke:#333,stroke-width:2px
+ style B3 fill:#FEF9E7,stroke:#333,stroke-width:2px
+ style C1 fill:#D6EAF8,stroke:#333,stroke-width:2px
+ style C2 fill:#D6EAF8,stroke:#333,stroke-width:2px
+ style D fill:#D5F5E3,stroke:#333,stroke-width:2px
+ style E fill:#FADBD8,stroke:#333,stroke-width:2px
+```
+
+- [`vite.config.ts`](./examples/starter/vite.config.ts)
+
+```js
+import rsc from '@vitejs/plugin-rsc'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ plugins: [
+ // add plugin
+ rsc(),
+ ],
+
+ // specify entry point for each environment.
+ environments: {
+ // `rsc` environment loads modules with `react-server` condition.
+ // this environment is responsible for:
+ // - RSC stream serialization (React VDOM -> RSC stream)
+ // - server functions handling
+ rsc: {
+ build: {
+ rollupOptions: {
+ input: {
+ index: './src/framework/entry.rsc.tsx',
+ },
+ },
+ },
+ },
+
+ // `ssr` environment loads modules without `react-server` condition.
+ // this environment is responsible for:
+ // - RSC stream deserialization (RSC stream -> React VDOM)
+ // - traditional SSR (React VDOM -> HTML string/stream)
+ ssr: {
+ build: {
+ rollupOptions: {
+ input: {
+ index: './src/framework/entry.ssr.tsx',
+ },
+ },
+ },
+ },
+
+ // client environment is used for hydration and client-side rendering
+ // this environment is responsible for:
+ // - RSC stream deserialization (RSC stream -> React VDOM)
+ // - traditional CSR (React VDOM -> Browser DOM tree mount/hydration)
+ // - refetch and re-render RSC
+ // - calling server functions
+ client: {
+ build: {
+ rollupOptions: {
+ input: {
+ index: './src/framework/entry.browser.tsx',
+ },
+ },
+ },
+ },
+ },
+})
+```
+
+- [`entry.rsc.tsx`](./examples/starter/src/framework/entry.rsc.tsx)
+
+```tsx
+import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc'
+
+// the plugin assumes `rsc` entry having default export of request handler
+export default async function handler(request: Request): Promise {
+ // serialize React VDOM to RSC stream
+ const root = (
+
+
+
Test
+
+
+ )
+ const rscStream = renderToReadableStream(root)
+
+ // respond direct RSC stream request based on framework's convention
+ if (request.url.endsWith('.rsc')) {
+ return new Response(rscStream, {
+ headers: {
+ 'Content-type': 'text/x-component;charset=utf-8',
+ },
+ })
+ }
+
+ // delegate to SSR environment for html rendering
+ // `loadModule` is a helper API provided by the plugin for multi environment interaction.
+ const ssrEntry = await import.meta.viteRsc.loadModule<
+ typeof import('./entry.ssr.tsx')
+ >('ssr', 'index')
+ const htmlStream = await ssrEntry.handleSsr(rscStream)
+
+ // respond html
+ return new Response(htmlStream, {
+ headers: {
+ 'Content-type': 'text/html',
+ },
+ })
+}
+
+// add `import.meta.hot.accept` to handle server module change efficiently
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
+```
+
+- [`entry.ssr.tsx`](./examples/starter/src/framework/entry.ssr.tsx)
+
+```tsx
+import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
+import { renderToReadableStream } from 'react-dom/server.edge'
+
+export async function handleSsr(rscStream: ReadableStream) {
+ // deserialize RSC stream back to React VDOM
+ const root = await createFromReadableStream(rscStream)
+
+ // helper API to allow referencing browser entry content from SSR environment
+ const bootstrapScriptContent =
+ await import.meta.viteRsc.loadBootstrapScriptContent('index')
+
+ // render html (traditional SSR)
+ const htmlStream = renderToReadableStream(root, {
+ bootstrapScriptContent,
+ })
+
+ return htmlStream
+}
+```
+
+- [`entry.browser.tsx`](./examples/starter/src/framework/entry.browser.tsx)
+
+```tsx
+import { createFromReadableStream } from '@vitejs/plugin-rsc/browser'
+import { hydrateRoot } from 'react-dom/client'
+
+async function main() {
+ // fetch and deserialize RSC stream back to React VDOM
+ const rscResponse = await fetch(window.location.href + '.rsc')
+ const root = await createFromReadableStream(rscResponse.body)
+
+ // hydration (traditional CSR)
+ hydrateRoot(document, root)
+}
+
+main()
+```
+
+## Environment helper API
+
+The plugin provides an additional helper for multi environment interaction.
+
+### Available on `rsc` or `ssr` environment
+
+#### `import.meta.viteRsc.loadModule`
+
+- Type: `(environmentName: "ssr" | "rsc", entryName: string) => Promise`
+
+This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa.
+
+During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process. When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process.
+
+During production build, this API will be rewritten into a static import of the specified entry of other environment build and the modules are executed inside the same runtime.
+
+For example,
+
+```js
+// ./entry.rsc.tsx
+const ssrModule = await import.meta.viteRsc.loadModule("ssr", "index");
+ssrModule.renderHTML(...);
+
+// ./entry.ssr.tsx (with environments.ssr.build.rollupOptions.input.index = "./entry.ssr.tsx")
+export function renderHTML(...) {}
+```
+
+### Available on `rsc` environment
+
+#### `import.meta.viteRsc.loadCss`
+
+> [!NOTE]
+> The plugin automatically injects CSS for server components. See the [CSS Support](#css-support) section for detailed information about automatic CSS injection.
+
+- Type: `(importer?: string) => React.ReactNode`
+
+This allows collecting css which is imported through a current server module and injecting them inside server components.
+
+```tsx
+import './test.css'
+import dep from './dep.tsx'
+
+export function ServerPage() {
+ // this will include css assets for "test.css"
+ // and any css transitively imported through "dep.tsx"
+ return (
+ <>
+ {import.meta.viteRsc.loadCss()}
+ ...
+ >
+ )
+}
+```
+
+When specifying `loadCss()`, it will collect css through the server module resolved by ``.
+
+```tsx
+// virtual:my-framework-helper
+export function Assets() {
+ return <>
+ {import.meta.viteRsc.loadCss("/routes/home.tsx")}
+ {import.meta.viteRsc.loadCss("/routes/about.tsx")}
+ {...}
+ >
+}
+
+// user-app.tsx
+import { Assets } from "virtual:my-framework-helper";
+
+export function UserApp() {
+ return
+
+
+
+ ...
+
+}
+```
+
+### Available on `ssr` environment
+
+#### `import.meta.viteRsc.loadBootstrapScriptContent("index")`
+
+This provides a raw js code to execute a browser entry file specified by `environments.client.build.rollupOptions.input.index`. This is intended to be used with React DOM SSR API, such as [`renderToReadableStream`](https://react.dev/reference/react-dom/server/renderToReadableStream)
+
+```js
+import { renderToReadableStream } from 'react-dom/server.edge'
+
+const bootstrapScriptContent =
+ await import.meta.viteRsc.loadBootstrapScriptContent('index')
+const htmlStream = await renderToReadableStream(reactNode, {
+ bootstrapScriptContent,
+})
+```
+
+### Available on `client` environment
+
+#### `rsc:update` event
+
+This event is fired when server modules are updated, which can be used to trigger re-fetching and re-rendering of RSC components on browser.
+
+```js
+import { createFromFetch } from '@vitejs/plugin-rsc/browser'
+
+import.meta.hot.on('rsc:update', async () => {
+ // re-fetch RSC stream
+ const rscPayload = await createFromFetch(fetch(window.location.href + '.rsc'))
+ // re-render ...
+})
+```
+
+## Plugin API
+
+### `@vitejs/plugin-rsc`
+
+- Type: `rsc: (options?: RscPluginOptions) => Plugin[]`;
+
+```js
+import rsc from '@vitejs/plugin-rsc'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ plugins: [
+ rsc({
+ // this is only a shorthand of specifying each rollup input via
+ // `environments[name].build.rollupOptions.input.index`
+ entries: {
+ rsc: '...',
+ ssr: '...',
+ client: '...',
+ },
+
+ // by default, the plugin sets up middleware
+ // using `default` export of `rsc` environment `index` entry.
+ // this behavior can be customized by `serverHandler` option.
+ serverHandler: false,
+
+ // the plugin provides build-time validation of 'server-only' and 'client-only' imports.
+ // this is enabled by default. See the "server-only and client-only import" section below for details.
+ validateImports: true,
+
+ // by default, the plugin uses a build-time generated encryption key for
+ // "use server" closure argument binding.
+ // This can be overwritten by configuring `defineEncryptionKey` option,
+ // for example, to obtain a key through environment variable during runtime.
+ // cf. https://nextjs.org/docs/app/guides/data-security#overwriting-encryption-keys-advanced
+ defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY',
+
+ // when `loadModuleDevProxy: true`, `import.meta.viteRsc.loadModule` is implemented
+ // through `fetch` based RPC, which allows, for example, rsc environment inside
+ // cloudflare workers to communicate with node ssr environment on main Vite process.
+ loadModuleDevProxy: true,
+
+ // by default, `loadCss()` helper is injected based on certain heuristics.
+ // if it breaks, it can be opt-out or selectively applied based on files.
+ rscCssTransform: { filter: (id) => id.includes('/my-app/') },
+
+ // see `RscPluginOptions` for full options ...
+ }),
+ ],
+ // the same options can be also specified via top-level `rsc` property.
+ // this allows other plugin to set options via `config` hook.
+ rsc: {
+ // ...
+ },
+})
+```
+
+## RSC runtime (react-server-dom) API
+
+### `@vitejs/plugin-rsc/rsc`
+
+This module re-exports RSC runtime API provided by `react-server-dom/server.edge` and `react-server-dom/client.edge` such as:
+
+- `renderToReadableStream`: RSC serialization (React VDOM -> RSC stream)
+- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM). This is also available on rsc environment itself. For example, it allows saving serialized RSC and deserializing it for later use.
+- `decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet`
+- `encodeReply/createClientTemporaryReferenceSet`
+
+### `@vitejs/plugin-rsc/ssr`
+
+This module re-exports RSC runtime API provided by `react-server-dom/client.edge`
+
+- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
+
+### `@vitejs/plugin-rsc/browser`
+
+This module re-exports RSC runtime API provided by `react-server-dom/client.browser`
+
+- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
+- `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)`
+- `encodeReply/setServerCallback`: server function related...
+
+## Tips
+
+### CSS Support
+
+The plugin automatically handles CSS code-splitting and injection for server components. This eliminates the need to manually call [`import.meta.viteRsc.loadCss()`](#importmetaviterscloadcss) in most cases.
+
+1. **Component Detection**: The plugin automatically detects server components by looking for:
+ - Function exports with capital letter names (e.g., `export function Page() {}`)
+ - Default exports that are functions with capital names (e.g., `export default function Page() {}`)
+ - Const exports assigned to functions with capital names (e.g., `export const Page = () => {}`)
+
+2. **CSS Import Detection**: For detected components, the plugin checks if the module imports any CSS files (`.css`, `.scss`, `.sass`, etc.)
+
+3. **Automatic Wrapping**: When both conditions are met, the plugin wraps the component with a CSS injection wrapper:
+
+```tsx
+// Before transformation
+import './styles.css'
+
+export function Page() {
+ return
Hello
+}
+
+// After transformation
+import './styles.css'
+
+export function Page() {
+ return (
+ <>
+ {import.meta.viteRsc.loadCss()}
+