Skip to content

Commit

Permalink
feat: add client events to import.meta.hot.on (#3638)
Browse files Browse the repository at this point in the history
Co-authored-by: Shinigami92 <chrissi92@hotmail.de>
Co-authored-by: Christopher Quadflieg <christopher.quadflieg@adsoul.com>
  • Loading branch information
3 people committed Jun 25, 2021
1 parent c45a02f commit de1ddd4
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 10 deletions.
11 changes: 10 additions & 1 deletion docs/guide/api-hmr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions packages/playground/hmr/__tests__/hmr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
8 changes: 8 additions & 0 deletions packages/playground/hmr/hmr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
26 changes: 25 additions & 1 deletion packages/vite/client.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
/// <reference lib="dom" />

import {
ConnectedPayload,
ErrorPayload,
FullReloadPayload,
PrunePayload,
UpdatePayload
} from 'types/hmrPayload'

interface ImportMeta {
url: string

Expand All @@ -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
<T extends string>(
event: CustomEventName<T>,
cb: (data: any) => void
): void
}
}

readonly env: ImportMetaEnv
Expand All @@ -47,6 +67,10 @@ interface ImportMetaEnv {
PROD: boolean
}

// See https://stackoverflow.com/a/63549561.
type CustomEventName<T extends string> = (T extends `vite:${T}` ? never : T) &
(`vite:${T}` extends T ? never : T)

// CSS modules
type CSSModuleClasses = { readonly [key: string]: string }

Expand Down
51 changes: 43 additions & 8 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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<any>, 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.
Expand All @@ -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)
Expand All @@ -120,6 +129,7 @@ async function handleMessage(payload: HMRPayload) {
})
break
case 'error': {
notifyListeners('vite:error', payload)
const err = payload.err
if (enableOverlay) {
createErrorOverlay(err)
Expand All @@ -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<T extends string>(
event: CustomEventName<T>,
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 string> = (T extends `vite:${T}` ? never : T) &
(`vite:${T}` extends T ? never : T)

const enableOverlay = __HMR_ENABLE_OVERLAY__

function createErrorOverlay(err: ErrorPayload['err']) {
Expand Down Expand Up @@ -333,10 +368,10 @@ const hotModulesMap = new Map<string, HotModule>()
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
const pruneMap = new Map<string, (data: any) => void | Promise<void>>()
const dataMap = new Map<string, any>()
const customListenersMap = new Map<string, ((customData: any) => void)[]>()
const customListenersMap = new Map<string, ((data: any) => void)[]>()
const ctxToListenersMap = new Map<
string,
Map<string, ((customData: any) => void)[]>
Map<string, ((data: any) => void)[]>
>()

// Just infer the return type for now
Expand Down Expand Up @@ -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<string, any[]>) => {
const existing = map.get(event) || []
existing.push(cb)
Expand Down

0 comments on commit de1ddd4

Please sign in to comment.