Skip to content

Commit de1ddd4

Browse files
fwoutsShinigami92Christopher Quadflieg
authored
feat: add client events to import.meta.hot.on (#3638)
Co-authored-by: Shinigami92 <[email protected]> Co-authored-by: Christopher Quadflieg <[email protected]>
1 parent c45a02f commit de1ddd4

File tree

5 files changed

+92
-10
lines changed

5 files changed

+92
-10
lines changed

docs/guide/api-hmr.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,13 @@ For now, calling `import.meta.hot.invalidate()` simply reloads the page.
112112
113113
## `hot.on(event, cb)`
114114
115-
Listen to a custom HMR event. Custom HMR events can be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details.
115+
Listen to an HMR event.
116+
117+
The following HMR events are dispatched by Vite automatically:
118+
- `'vite:beforeUpdate'` when an update is about to be applied (e.g. a module will be replaced)
119+
- `'vite:beforeFullReload'` when a full reload is about to occur
120+
- `'vite:beforePrune'` when modules that are no longer needed are about to be pruned
121+
- `'vite:error'` when an error occurs (e.g. syntax error)
122+
123+
Custom HMR events can also be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details.
124+

packages/playground/hmr/__tests__/hmr.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ if (!isBuild) {
2020
await untilUpdated(() => el.textContent(), '2')
2121

2222
expect(browserLogs).toMatchObject([
23+
'>>> vite:beforeUpdate -- update',
2324
'foo was: 1',
2425
'(self-accepting 1) foo is now: 2',
2526
'(self-accepting 2) foo is now: 2',
@@ -31,6 +32,7 @@ if (!isBuild) {
3132
await untilUpdated(() => el.textContent(), '3')
3233

3334
expect(browserLogs).toMatchObject([
35+
'>>> vite:beforeUpdate -- update',
3436
'foo was: 2',
3537
'(self-accepting 1) foo is now: 3',
3638
'(self-accepting 2) foo is now: 3',
@@ -48,6 +50,7 @@ if (!isBuild) {
4850
await untilUpdated(() => el.textContent(), '2')
4951

5052
expect(browserLogs).toMatchObject([
53+
'>>> vite:beforeUpdate -- update',
5154
'(dep) foo was: 1',
5255
'(dep) foo from dispose: 1',
5356
'(single dep) foo is now: 2',
@@ -64,6 +67,7 @@ if (!isBuild) {
6467
await untilUpdated(() => el.textContent(), '3')
6568

6669
expect(browserLogs).toMatchObject([
70+
'>>> vite:beforeUpdate -- update',
6771
'(dep) foo was: 2',
6872
'(dep) foo from dispose: 2',
6973
'(single dep) foo is now: 3',
@@ -84,6 +88,7 @@ if (!isBuild) {
8488
await untilUpdated(() => el.textContent(), '2')
8589

8690
expect(browserLogs).toMatchObject([
91+
'>>> vite:beforeUpdate -- update',
8792
'(dep) foo was: 3',
8893
'(dep) foo from dispose: 3',
8994
'(single dep) foo is now: 3',
@@ -100,6 +105,7 @@ if (!isBuild) {
100105
await untilUpdated(() => el.textContent(), '3')
101106

102107
expect(browserLogs).toMatchObject([
108+
'>>> vite:beforeUpdate -- update',
103109
'(dep) foo was: 3',
104110
'(dep) foo from dispose: 3',
105111
'(single dep) foo is now: 3',

packages/playground/hmr/hmr.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ if (import.meta.hot) {
3333
console.log(`foo was:`, foo)
3434
})
3535

36+
import.meta.hot.on('vite:beforeUpdate', (event) => {
37+
console.log(`>>> vite:beforeUpdate -- ${event.type}`)
38+
})
39+
40+
import.meta.hot.on('vite:error', (event) => {
41+
console.log(`>>> vite:error -- ${event.type}`)
42+
})
43+
3644
import.meta.hot.on('foo', ({ msg }) => {
3745
text('.custom', msg)
3846
})

packages/vite/client.d.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/// <reference lib="dom" />
22

3+
import {
4+
ConnectedPayload,
5+
ErrorPayload,
6+
FullReloadPayload,
7+
PrunePayload,
8+
UpdatePayload
9+
} from 'types/hmrPayload'
10+
311
interface ImportMeta {
412
url: string
513

@@ -20,7 +28,19 @@ interface ImportMeta {
2028
decline(): void
2129
invalidate(): void
2230

23-
on(event: string, cb: (...args: any[]) => void): void
31+
on: {
32+
(event: 'vite:beforeUpdate', cb: (payload: UpdatePayload) => void): void
33+
(event: 'vite:beforePrune', cb: (payload: PrunePayload) => void): void
34+
(
35+
event: 'vite:beforeFullReload',
36+
cb: (payload: FullReloadPayload) => void
37+
): void
38+
(event: 'vite:error', cb: (payload: ErrorPayload) => void): void
39+
<T extends string>(
40+
event: CustomEventName<T>,
41+
cb: (data: any) => void
42+
): void
43+
}
2444
}
2545

2646
readonly env: ImportMetaEnv
@@ -47,6 +67,10 @@ interface ImportMetaEnv {
4767
PROD: boolean
4868
}
4969

70+
// See https://stackoverflow.com/a/63549561.
71+
type CustomEventName<T extends string> = (T extends `vite:${T}` ? never : T) &
72+
(`vite:${T}` extends T ? never : T)
73+
5074
// CSS modules
5175
type CSSModuleClasses = { readonly [key: string]: string }
5276

packages/vite/src/client/client.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload'
1+
import {
2+
ConnectedPayload,
3+
ErrorPayload,
4+
FullReloadPayload,
5+
HMRPayload,
6+
PrunePayload,
7+
Update,
8+
UpdatePayload
9+
} from 'types/hmrPayload'
210
import { ErrorOverlay, overlayId } from './overlay'
311
import './env'
12+
413
// injected by the hmr plugin when served
514
declare const __ROOT__: string
615
declare const __BASE__: string
@@ -46,6 +55,7 @@ async function handleMessage(payload: HMRPayload) {
4655
setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
4756
break
4857
case 'update':
58+
notifyListeners('vite:beforeUpdate', payload)
4959
// if this is the first update and there's already an error overlay, it
5060
// means the page opened with existing server compile error and the whole
5161
// module script failed to load (since one of the nested imports is 500).
@@ -84,13 +94,11 @@ async function handleMessage(payload: HMRPayload) {
8494
})
8595
break
8696
case 'custom': {
87-
const cbs = customListenersMap.get(payload.event)
88-
if (cbs) {
89-
cbs.forEach((cb) => cb(payload.data))
90-
}
97+
notifyListeners(payload.event as CustomEventName<any>, payload.data)
9198
break
9299
}
93100
case 'full-reload':
101+
notifyListeners('vite:beforeFullReload', payload)
94102
if (payload.path && payload.path.endsWith('.html')) {
95103
// if html file is edited, only reload the page if the browser is
96104
// currently on that page.
@@ -108,6 +116,7 @@ async function handleMessage(payload: HMRPayload) {
108116
}
109117
break
110118
case 'prune':
119+
notifyListeners('vite:beforePrune', payload)
111120
// After an HMR update, some modules are no longer imported on the page
112121
// but they may have left behind side effects that need to be cleaned up
113122
// (.e.g style injections)
@@ -120,6 +129,7 @@ async function handleMessage(payload: HMRPayload) {
120129
})
121130
break
122131
case 'error': {
132+
notifyListeners('vite:error', payload)
123133
const err = payload.err
124134
if (enableOverlay) {
125135
createErrorOverlay(err)
@@ -137,6 +147,31 @@ async function handleMessage(payload: HMRPayload) {
137147
}
138148
}
139149

150+
function notifyListeners(
151+
event: 'vite:beforeUpdate',
152+
payload: UpdatePayload
153+
): void
154+
function notifyListeners(event: 'vite:beforePrune', payload: PrunePayload): void
155+
function notifyListeners(
156+
event: 'vite:beforeFullReload',
157+
payload: FullReloadPayload
158+
): void
159+
function notifyListeners(event: 'vite:error', payload: ErrorPayload): void
160+
function notifyListeners<T extends string>(
161+
event: CustomEventName<T>,
162+
data: any
163+
): void
164+
function notifyListeners(event: string, data: any): void {
165+
const cbs = customListenersMap.get(event)
166+
if (cbs) {
167+
cbs.forEach((cb) => cb(data))
168+
}
169+
}
170+
171+
// See https://stackoverflow.com/a/63549561.
172+
type CustomEventName<T extends string> = (T extends `vite:${T}` ? never : T) &
173+
(`vite:${T}` extends T ? never : T)
174+
140175
const enableOverlay = __HMR_ENABLE_OVERLAY__
141176

142177
function createErrorOverlay(err: ErrorPayload['err']) {
@@ -333,10 +368,10 @@ const hotModulesMap = new Map<string, HotModule>()
333368
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
334369
const pruneMap = new Map<string, (data: any) => void | Promise<void>>()
335370
const dataMap = new Map<string, any>()
336-
const customListenersMap = new Map<string, ((customData: any) => void)[]>()
371+
const customListenersMap = new Map<string, ((data: any) => void)[]>()
337372
const ctxToListenersMap = new Map<
338373
string,
339-
Map<string, ((customData: any) => void)[]>
374+
Map<string, ((data: any) => void)[]>
340375
>()
341376

342377
// Just infer the return type for now
@@ -427,7 +462,7 @@ export const createHotContext = (ownerPath: string) => {
427462
},
428463

429464
// custom events
430-
on(event: string, cb: () => void) {
465+
on: (event: string, cb: (data: any) => void) => {
431466
const addToMap = (map: Map<string, any[]>) => {
432467
const existing = map.get(event) || []
433468
existing.push(cb)

0 commit comments

Comments
 (0)