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)