Skip to content

Commit

Permalink
fix: support HMR for class components (#320)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudBarre authored May 22, 2024
1 parent 302a323 commit 2b7f2ae
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 8 deletions.
4 changes: 4 additions & 0 deletions packages/plugin-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Support HMR for class components

This is a long overdue and should fix some issues people had with HMR when migrating from CRA.

## 4.2.1 (2023-12-04)

Remove generic parameter on `Plugin` to avoid type error with Rollup 4/Vite 5 and `skipLibCheck: false`.
Expand Down
25 changes: 20 additions & 5 deletions packages/plugin-react/src/fast-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
`

const header = `
const sharedHeader = `
import RefreshRuntime from "${runtimePublicPath}";
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
`.replace(/\n+/g, '')
const functionHeader = `
let prevRefreshReg;
let prevRefreshSig;
Expand All @@ -51,11 +53,13 @@ if (import.meta.hot && !inWebWorker) {
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}`.replace(/\n+/g, '')

const footer = `
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) => {
Expand All @@ -68,8 +72,19 @@ if (import.meta.hot && !inWebWorker) {

export function addRefreshWrapper(code: string, id: string): string {
return (
header.replace('__SOURCE__', JSON.stringify(id)) +
sharedHeader +
functionHeader.replace('__SOURCE__', JSON.stringify(id)) +
code +
footer.replace('__SOURCE__', JSON.stringify(id))
functionFooter +
sharedFooter.replace('__SOURCE__', JSON.stringify(id))
)
}

export function addClassComponentRefreshWrapper(
code: string,
id: string,
): string {
return (
sharedHeader + code + sharedFooter.replace('__SOURCE__', JSON.stringify(id))
)
}
10 changes: 8 additions & 2 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
UserConfig,
} from 'vite'
import {
addClassComponentRefreshWrapper,
addRefreshWrapper,
preambleCode,
runtimeCode,
Expand Down Expand Up @@ -86,6 +87,7 @@ export type ViteReactPluginApi = {
reactBabel?: ReactBabelHook
}

const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
const defaultIncludeRE = /\.[tj]sx?$/
const tsRE = /\.tsx?$/
Expand Down Expand Up @@ -250,8 +252,12 @@ export default function viteReact(opts: Options = {}): PluginOption[] {

if (result) {
let code = result.code!
if (useFastRefresh && refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
if (useFastRefresh) {
if (refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
} else if (reactCompRE.test(code)) {
code = addClassComponentRefreshWrapper(code, id)
}
}
return { code, map: result.map }
}
Expand Down
30 changes: 30 additions & 0 deletions playground/class-components/__tests__/class-components.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from 'vitest'
import {
editFile,
isServe,
page,
untilBrowserLogAfter,
untilUpdated,
} from '~utils'

test('should render', async () => {
expect(await page.textContent('span')).toMatch('Hello World')
})

if (isServe) {
test('Class component HMR', async () => {
editFile('src/App.tsx', (code) => code.replace('World', 'class components'))
await untilBrowserLogAfter(
() => page.textContent('span'),
'[vite] hot updated: /src/App.tsx',
)
await untilUpdated(() => page.textContent('span'), 'Hello class components')

editFile('src/utils.tsx', (code) => code.replace('Hello', 'Hi'))
await untilBrowserLogAfter(
() => page.textContent('span'),
'[vite] hot updated: /src/App.tsx',
)
await untilUpdated(() => page.textContent('span'), 'Hi class components')
})
}
13 changes: 13 additions & 0 deletions playground/class-components/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + class components</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions playground/class-components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@vitejs/test-class-components",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "workspace:*"
}
}
1 change: 1 addition & 0 deletions playground/class-components/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions playground/class-components/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from 'react'
import { getGetting } from './utils'

export class App extends Component {
render() {
return <span>{getGetting()} World</span>
}
}
9 changes: 9 additions & 0 deletions playground/class-components/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
1 change: 1 addition & 0 deletions playground/class-components/src/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getGetting = () => <span>Hello</span>
23 changes: 23 additions & 0 deletions playground/class-components/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"include": ["src"],
"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
}
}
7 changes: 7 additions & 0 deletions playground/class-components/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
server: { port: 8908 /* Should be unique */ },
plugins: [react()],
})
2 changes: 1 addition & 1 deletion playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["."],
"include": ["*.ts", "**/__test/*.ts", "**/vite.config.ts"],
"exclude": ["**/dist/**"],
"compilerOptions": {
"target": "ES2020",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2b7f2ae

Please sign in to comment.