Skip to content

Commit

Permalink
feat(type): support typing for custom events (#7476)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Mar 26, 2022
1 parent 81d7dd2 commit 50a8765
Show file tree
Hide file tree
Showing 12 changed files with 96 additions and 75 deletions.
16 changes: 16 additions & 0 deletions docs/guide/api-plugin.md
Expand Up @@ -560,3 +560,19 @@ export default defineConfig({
]
})
```
### TypeScript for Custom Events
It is possible to type custom events by extending the `CustomEventMap` interface:
```ts
// events.d.ts
import 'vite/types/customEvent'

declare module 'vite/types/customEvent' {
interface CustomEventMap {
'custom:foo': { msg: string }
// 'event-key': payload
}
}
```
16 changes: 8 additions & 8 deletions packages/playground/hmr/__tests__/hmr.spec.ts
Expand Up @@ -16,27 +16,27 @@ if (!isBuild) {
test('self accept', async () => {
const el = await page.$('.app')

editFile('hmr.js', (code) => code.replace('const foo = 1', 'const foo = 2'))
editFile('hmr.ts', (code) => code.replace('const foo = 1', 'const foo = 2'))
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',
'[vite] hot updated: /hmr.js'
'[vite] hot updated: /hmr.ts'
])
browserLogs.length = 0

editFile('hmr.js', (code) => code.replace('const foo = 2', 'const foo = 3'))
editFile('hmr.ts', (code) => code.replace('const foo = 2', 'const foo = 3'))
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',
'[vite] hot updated: /hmr.js'
'[vite] hot updated: /hmr.ts'
])
browserLogs.length = 0
})
Expand All @@ -57,7 +57,7 @@ if (!isBuild) {
'(single dep) nested foo is now: 1',
'(multi deps) foo is now: 2',
'(multi deps) nested foo is now: 1',
'[vite] hot updated: /hmrDep.js via /hmr.js'
'[vite] hot updated: /hmrDep.js via /hmr.ts'
])
browserLogs.length = 0

Expand All @@ -74,7 +74,7 @@ if (!isBuild) {
'(single dep) nested foo is now: 1',
'(multi deps) foo is now: 3',
'(multi deps) nested foo is now: 1',
'[vite] hot updated: /hmrDep.js via /hmr.js'
'[vite] hot updated: /hmrDep.js via /hmr.ts'
])
browserLogs.length = 0
})
Expand All @@ -95,7 +95,7 @@ if (!isBuild) {
'(single dep) nested foo is now: 2',
'(multi deps) foo is now: 3',
'(multi deps) nested foo is now: 2',
'[vite] hot updated: /hmrDep.js via /hmr.js'
'[vite] hot updated: /hmrDep.js via /hmr.ts'
])
browserLogs.length = 0

Expand All @@ -112,7 +112,7 @@ if (!isBuild) {
'(single dep) nested foo is now: 3',
'(multi deps) foo is now: 3',
'(multi deps) nested foo is now: 3',
'[vite] hot updated: /hmrDep.js via /hmr.js'
'[vite] hot updated: /hmrDep.js via /hmr.ts'
])
browserLogs.length = 0
})
Expand Down
9 changes: 9 additions & 0 deletions packages/playground/hmr/event.d.ts
@@ -0,0 +1,9 @@
import 'vite/types/customEvent'

declare module 'vite/types/customEvent' {
interface CustomEventMap {
'custom:foo': { msg: string }
'custom:remote-add': { a: number; b: number }
'custom:remote-add-result': { result: string }
}
}
Expand Up @@ -41,7 +41,7 @@ if (import.meta.hot) {
update.type === 'css-update' && update.path.match('global.css')
)
if (cssUpdate) {
const el = document.querySelector('#global-css')
const el = document.querySelector('#global-css') as HTMLLinkElement
text('.css-prev', el.href)
// We don't have a vite:afterUpdate event, but updates are currently sync
setTimeout(() => {
Expand All @@ -54,13 +54,13 @@ if (import.meta.hot) {
console.log(`>>> vite:error -- ${event.type}`)
})

import.meta.hot.on('foo', ({ msg }) => {
import.meta.hot.on('custom:foo', ({ msg }) => {
text('.custom', msg)
})

// send custom event to server to calculate 1 + 2
import.meta.hot.send('remote-add', { a: 1, b: 2 })
import.meta.hot.on('remote-add-result', ({ result }) => {
import.meta.hot.send('custom:remote-add', { a: 1, b: 2 })
import.meta.hot.on('custom:remote-add-result', ({ result }) => {
text('.custom-communication', result)
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/hmr/index.html
@@ -1,5 +1,5 @@
<link id="global-css" rel="stylesheet" href="./global.css?param=required" />
<script type="module" src="./hmr.js"></script>
<script type="module" src="./hmr.ts"></script>

<div class="app"></div>
<div class="dep"></div>
Expand Down
15 changes: 15 additions & 0 deletions packages/playground/hmr/tsconfig.json
@@ -0,0 +1,15 @@
{
"include": ["."],
"exclude": ["**/dist/**"],
"compilerOptions": {
"target": "es2019",
"module": "esnext",
"outDir": "dist",
"allowJs": true,
"esModuleInterop": true,
"moduleResolution": "node",
"baseUrl": ".",
"jsx": "preserve",
"types": ["vite/client", "jest", "node"]
}
}
@@ -1,22 +1,21 @@
/**
* @type {import('vite').UserConfig}
*/
module.exports = {
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
{
name: 'mock-custom',
async handleHotUpdate({ file, read, server }) {
if (file.endsWith('customFile.js')) {
const content = await read()
const msg = content.match(/export const msg = '(\w+)'/)[1]
server.ws.send('foo', { msg })
server.ws.send('custom:foo', { msg })
}
},
configureServer(server) {
server.ws.on('remote-add', ({ a, b }, client) => {
client.send('remote-add-result', { result: a + b })
server.ws.on('custom:remote-add', ({ a, b }, client) => {
client.send('custom:remote-add-result', { result: a + b })
})
}
}
]
}
})
27 changes: 5 additions & 22 deletions packages/vite/src/client/client.ts
@@ -1,13 +1,6 @@
import type {
ErrorPayload,
FullReloadPayload,
HMRPayload,
PrunePayload,
Update,
UpdatePayload
} from 'types/hmrPayload'
import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload'
import type { ViteHotContext } from 'types/hot'
import type { CustomEventName } from 'types/customEvent'
import type { InferCustomEventPayload } from 'types/customEvent'
import { ErrorOverlay, overlayId } from './overlay'
// eslint-disable-next-line node/no-missing-import
import '@vite/env'
Expand Down Expand Up @@ -104,7 +97,7 @@ async function handleMessage(payload: HMRPayload) {
})
break
case 'custom': {
notifyListeners(payload.event as CustomEventName<any>, payload.data)
notifyListeners(payload.event, payload.data)
break
}
case 'full-reload':
Expand Down Expand Up @@ -157,19 +150,9 @@ 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
event: T,
data: InferCustomEventPayload<T>
): void
function notifyListeners(event: string, data: any): void {
const cbs = customListenersMap.get(event)
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/index.ts
Expand Up @@ -108,6 +108,7 @@ export type {
export type { Terser } from 'types/terser'
export type { RollupCommonJSOptions } from 'types/commonjs'
export type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars'
export type { CustomEventMap, InferCustomEventPayload } from 'types/customEvent'
export type { Matcher, AnymatchPattern, AnymatchFn } from 'types/anymatch'
export type { SplitVendorChunkCache } from './plugins/splitVendorChunk'

Expand Down
11 changes: 8 additions & 3 deletions packages/vite/src/node/server/ws.ts
Expand Up @@ -6,9 +6,11 @@ import { createServer as createHttpsServer } from 'https'
import type { ServerOptions, WebSocket as WebSocketRaw } from 'ws'
import { WebSocketServer as WebSocketServerRaw } from 'ws'
import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload'
import type { InferCustomEventPayload } from 'types/customEvent'
import type { ResolvedConfig } from '..'
import { isObject } from '../utils'
import type { Socket } from 'net'

export const HMR_HEADER = 'vite-hmr'

export type WebSocketCustomListener<T> = (
Expand All @@ -28,7 +30,7 @@ export interface WebSocketServer {
/**
* Send custom event
*/
send(event: string, payload?: CustomPayload['data']): void
send<T extends string>(event: T, payload?: InferCustomEventPayload<T>): void
/**
* Disconnect all clients and terminate the server.
*/
Expand All @@ -37,13 +39,16 @@ export interface WebSocketServer {
* Handle custom event emitted by `import.meta.hot.send`
*/
on: WebSocketServerRaw['on'] & {
(event: string, listener: WebSocketCustomListener<any>): void
<T extends string>(
event: T,
listener: WebSocketCustomListener<InferCustomEventPayload<T>>
): void
}
/**
* Unregister event listener.
*/
off: WebSocketServerRaw['off'] & {
(event: string, listener: WebSocketCustomListener<any>): void
(event: string, listener: Function): void
}
}

Expand Down
21 changes: 16 additions & 5 deletions packages/vite/types/customEvent.d.ts
@@ -1,5 +1,16 @@
// See https://stackoverflow.com/a/63549561.
export type CustomEventName<T extends string> = (T extends `vite:${T}`
? never
: T) &
(`vite:${T}` extends T ? never : T)
import type {
ErrorPayload,
FullReloadPayload,
PrunePayload,
UpdatePayload
} from './hmrPayload'

export interface CustomEventMap {
'vite:beforeUpdate': UpdatePayload
'vite:beforePrune': PrunePayload
'vite:beforeFullReload': FullReloadPayload
'vite:error': ErrorPayload
}

export type InferCustomEventPayload<T extends string> =
T extends keyof CustomEventMap ? CustomEventMap[T] : unknown
30 changes: 6 additions & 24 deletions packages/vite/types/hot.d.ts
@@ -1,9 +1,4 @@
import type {
ErrorPayload,
FullReloadPayload,
PrunePayload,
UpdatePayload
} from './hmrPayload'
import type { InferCustomEventPayload } from './customEvent'

export interface ViteHotContext {
readonly data: any
Expand All @@ -22,22 +17,9 @@ export interface ViteHotContext {
decline(): void
invalidate(): 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: string, cb: (data: any) => void): void
}

send(event: string, data?: any): void
on<T extends string>(
event: T,
cb: (payload: InferCustomEventPayload<T>) => void
): void
send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void
}

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

0 comments on commit 50a8765

Please sign in to comment.