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(type): support typing for custom events #7476

Merged
merged 6 commits into from
Mar 26, 2022
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
16 changes: 16 additions & 0 deletions docs/guide/api-plugin.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"]
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)