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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)