Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add client events to import.meta.hot.on #3638

Merged
merged 14 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +71 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh 🤔 if custom events from plugins can also be send, should we enforce the type to start with vite: always?

@patak-js What do you think?

Otherwise I now approve this PR from my side

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I understand the question, we should enforce vite: for our events, and custom events are free to use their own naming convention.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I understand the question, we should enforce vite: for our events, and custom events are free to use their own naming convention.


// 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 @@ -135,6 +145,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 @@ -331,10 +366,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 @@ -425,7 +460,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