diff --git a/docs/guide/api-hmr.md b/docs/guide/api-hmr.md
index 829c53a7f1ccc3..4203e310c63ce9 100644
--- a/docs/guide/api-hmr.md
+++ b/docs/guide/api-hmr.md
@@ -112,4 +112,13 @@ For now, calling `import.meta.hot.invalidate()` simply reloads the page.
## `hot.on(event, cb)`
-Listen to a custom HMR event. Custom HMR events can be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details.
+Listen to an HMR event.
+
+The following HMR events are dispatched by Vite automatically:
+- `'vite:beforeUpdate'` when an update is about to be applied (e.g. a module will be replaced)
+- `'vite:beforeFullReload'` when a full reload is about to occur
+- `'vite:beforePrune'` when modules that are no longer needed are about to be pruned
+- `'vite:error'` when an error occurs (e.g. syntax error)
+
+Custom HMR events can also be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details.
+
diff --git a/packages/playground/hmr/__tests__/hmr.spec.ts b/packages/playground/hmr/__tests__/hmr.spec.ts
index 403610e8c9d76f..d0c78d9ee3f9f0 100644
--- a/packages/playground/hmr/__tests__/hmr.spec.ts
+++ b/packages/playground/hmr/__tests__/hmr.spec.ts
@@ -20,6 +20,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '2')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'foo was: 1',
'(self-accepting 1) foo is now: 2',
'(self-accepting 2) foo is now: 2',
@@ -31,6 +32,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '3')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'foo was: 2',
'(self-accepting 1) foo is now: 3',
'(self-accepting 2) foo is now: 3',
@@ -48,6 +50,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '2')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'(dep) foo was: 1',
'(dep) foo from dispose: 1',
'(single dep) foo is now: 2',
@@ -64,6 +67,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '3')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'(dep) foo was: 2',
'(dep) foo from dispose: 2',
'(single dep) foo is now: 3',
@@ -84,6 +88,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '2')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'(dep) foo was: 3',
'(dep) foo from dispose: 3',
'(single dep) foo is now: 3',
@@ -100,6 +105,7 @@ if (!isBuild) {
await untilUpdated(() => el.textContent(), '3')
expect(browserLogs).toMatchObject([
+ '>>> vite:beforeUpdate -- update',
'(dep) foo was: 3',
'(dep) foo from dispose: 3',
'(single dep) foo is now: 3',
diff --git a/packages/playground/hmr/hmr.js b/packages/playground/hmr/hmr.js
index 0df01dbee0f5d0..e8da1ecbabf98b 100644
--- a/packages/playground/hmr/hmr.js
+++ b/packages/playground/hmr/hmr.js
@@ -33,6 +33,14 @@ if (import.meta.hot) {
console.log(`foo was:`, foo)
})
+ import.meta.hot.on('vite:beforeUpdate', (event) => {
+ console.log(`>>> vite:beforeUpdate -- ${event.type}`)
+ })
+
+ import.meta.hot.on('vite:error', (event) => {
+ console.log(`>>> vite:error -- ${event.type}`)
+ })
+
import.meta.hot.on('foo', ({ msg }) => {
text('.custom', msg)
})
diff --git a/packages/vite/client.d.ts b/packages/vite/client.d.ts
index e41118c4dbc2c2..bddb1bcb7c4bb4 100644
--- a/packages/vite/client.d.ts
+++ b/packages/vite/client.d.ts
@@ -1,5 +1,13 @@
///
+import {
+ ConnectedPayload,
+ ErrorPayload,
+ FullReloadPayload,
+ PrunePayload,
+ UpdatePayload
+} from 'types/hmrPayload'
+
interface ImportMeta {
url: string
@@ -20,7 +28,19 @@ interface ImportMeta {
decline(): void
invalidate(): void
- on(event: string, cb: (...args: any[]) => void): void
+ on: {
+ (event: 'vite:beforeUpdate', cb: (payload: UpdatePayload) => void): void
+ (event: 'vite:beforePrune', cb: (payload: PrunePayload) => void): void
+ (
+ event: 'vite:beforeFullReload',
+ cb: (payload: FullReloadPayload) => void
+ ): void
+ (event: 'vite:error', cb: (payload: ErrorPayload) => void): void
+ (
+ event: CustomEventName,
+ cb: (data: any) => void
+ ): void
+ }
}
readonly env: ImportMetaEnv
@@ -47,6 +67,10 @@ interface ImportMetaEnv {
PROD: boolean
}
+// See https://stackoverflow.com/a/63549561.
+type CustomEventName = (T extends `vite:${T}` ? never : T) &
+ (`vite:${T}` extends T ? never : T)
+
// CSS modules
type CSSModuleClasses = { readonly [key: string]: string }
diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts
index 387767991de84f..1b276c95e0b9d0 100644
--- a/packages/vite/src/client/client.ts
+++ b/packages/vite/src/client/client.ts
@@ -1,6 +1,15 @@
-import { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload'
+import {
+ ConnectedPayload,
+ ErrorPayload,
+ FullReloadPayload,
+ HMRPayload,
+ PrunePayload,
+ Update,
+ UpdatePayload
+} from 'types/hmrPayload'
import { ErrorOverlay, overlayId } from './overlay'
import './env'
+
// injected by the hmr plugin when served
declare const __ROOT__: string
declare const __BASE__: string
@@ -46,6 +55,7 @@ async function handleMessage(payload: HMRPayload) {
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
break
case 'update':
+ notifyListeners('vite:beforeUpdate', payload)
// if this is the first update and there's already an error overlay, it
// means the page opened with existing server compile error and the whole
// module script failed to load (since one of the nested imports is 500).
@@ -84,13 +94,11 @@ async function handleMessage(payload: HMRPayload) {
})
break
case 'custom': {
- const cbs = customListenersMap.get(payload.event)
- if (cbs) {
- cbs.forEach((cb) => cb(payload.data))
- }
+ notifyListeners(payload.event as CustomEventName, payload.data)
break
}
case 'full-reload':
+ notifyListeners('vite:beforeFullReload', payload)
if (payload.path && payload.path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
@@ -108,6 +116,7 @@ async function handleMessage(payload: HMRPayload) {
}
break
case 'prune':
+ notifyListeners('vite:beforePrune', payload)
// After an HMR update, some modules are no longer imported on the page
// but they may have left behind side effects that need to be cleaned up
// (.e.g style injections)
@@ -120,6 +129,7 @@ async function handleMessage(payload: HMRPayload) {
})
break
case 'error': {
+ notifyListeners('vite:error', payload)
const err = payload.err
if (enableOverlay) {
createErrorOverlay(err)
@@ -137,6 +147,31 @@ async function handleMessage(payload: HMRPayload) {
}
}
+function notifyListeners(
+ event: 'vite:beforeUpdate',
+ payload: UpdatePayload
+): void
+function notifyListeners(event: 'vite:beforePrune', payload: PrunePayload): void
+function notifyListeners(
+ event: 'vite:beforeFullReload',
+ payload: FullReloadPayload
+): void
+function notifyListeners(event: 'vite:error', payload: ErrorPayload): void
+function notifyListeners(
+ event: CustomEventName,
+ data: any
+): void
+function notifyListeners(event: string, data: any): void {
+ const cbs = customListenersMap.get(event)
+ if (cbs) {
+ cbs.forEach((cb) => cb(data))
+ }
+}
+
+// See https://stackoverflow.com/a/63549561.
+type CustomEventName = (T extends `vite:${T}` ? never : T) &
+ (`vite:${T}` extends T ? never : T)
+
const enableOverlay = __HMR_ENABLE_OVERLAY__
function createErrorOverlay(err: ErrorPayload['err']) {
@@ -333,10 +368,10 @@ const hotModulesMap = new Map()
const disposeMap = new Map void | Promise>()
const pruneMap = new Map void | Promise>()
const dataMap = new Map()
-const customListenersMap = new Map void)[]>()
+const customListenersMap = new Map void)[]>()
const ctxToListenersMap = new Map<
string,
- Map void)[]>
+ Map void)[]>
>()
// Just infer the return type for now
@@ -427,7 +462,7 @@ export const createHotContext = (ownerPath: string) => {
},
// custom events
- on(event: string, cb: () => void) {
+ on: (event: string, cb: (data: any) => void) => {
const addToMap = (map: Map) => {
const existing = map.get(event) || []
existing.push(cb)