diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index 13767c45dd3103..228755dd6f85c2 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -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 + } +} +``` diff --git a/packages/playground/hmr/__tests__/hmr.spec.ts b/packages/playground/hmr/__tests__/hmr.spec.ts index 4d0491af91a69e..6ddc2345ae4fb4 100644 --- a/packages/playground/hmr/__tests__/hmr.spec.ts +++ b/packages/playground/hmr/__tests__/hmr.spec.ts @@ -16,7 +16,7 @@ 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([ @@ -24,11 +24,11 @@ if (!isBuild) { '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([ @@ -36,7 +36,7 @@ if (!isBuild) { '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 }) @@ -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 @@ -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 }) @@ -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 @@ -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 }) diff --git a/packages/playground/hmr/event.d.ts b/packages/playground/hmr/event.d.ts new file mode 100644 index 00000000000000..151a9cc3b861cd --- /dev/null +++ b/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 } + } +} diff --git a/packages/playground/hmr/hmr.js b/packages/playground/hmr/hmr.ts similarity index 87% rename from packages/playground/hmr/hmr.js rename to packages/playground/hmr/hmr.ts index e80b517e6449dc..113b87bc5865d4 100644 --- a/packages/playground/hmr/hmr.js +++ b/packages/playground/hmr/hmr.ts @@ -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(() => { @@ -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) }) } diff --git a/packages/playground/hmr/index.html b/packages/playground/hmr/index.html index fc398c60c4cadf..0add7c26011a01 100644 --- a/packages/playground/hmr/index.html +++ b/packages/playground/hmr/index.html @@ -1,5 +1,5 @@ - +
diff --git a/packages/playground/hmr/tsconfig.json b/packages/playground/hmr/tsconfig.json new file mode 100644 index 00000000000000..41b16fdc65ec8c --- /dev/null +++ b/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"] + } +} diff --git a/packages/playground/hmr/vite.config.js b/packages/playground/hmr/vite.config.ts similarity index 56% rename from packages/playground/hmr/vite.config.js rename to packages/playground/hmr/vite.config.ts index 57252c91be410b..ef5d3cf36a2fcb 100644 --- a/packages/playground/hmr/vite.config.js +++ b/packages/playground/hmr/vite.config.ts @@ -1,7 +1,6 @@ -/** - * @type {import('vite').UserConfig} - */ -module.exports = { +import { defineConfig } from 'vite' + +export default defineConfig({ plugins: [ { name: 'mock-custom', @@ -9,14 +8,14 @@ module.exports = { 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 }) }) } } ] -} +}) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 1fe461f5b04caf..c180714f5a69bf 100644 --- a/packages/vite/src/client/client.ts +++ b/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' @@ -104,7 +97,7 @@ async function handleMessage(payload: HMRPayload) { }) break case 'custom': { - notifyListeners(payload.event as CustomEventName, payload.data) + notifyListeners(payload.event, payload.data) break } case 'full-reload': @@ -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( - event: CustomEventName, - data: any + event: T, + data: InferCustomEventPayload ): void function notifyListeners(event: string, data: any): void { const cbs = customListenersMap.get(event) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 2b59da8737029b..2e849d846527ca 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -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' diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 3c6875e2475c64..a01d0bd3225571 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -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 = ( @@ -28,7 +30,7 @@ export interface WebSocketServer { /** * Send custom event */ - send(event: string, payload?: CustomPayload['data']): void + send(event: T, payload?: InferCustomEventPayload): void /** * Disconnect all clients and terminate the server. */ @@ -37,13 +39,16 @@ export interface WebSocketServer { * Handle custom event emitted by `import.meta.hot.send` */ on: WebSocketServerRaw['on'] & { - (event: string, listener: WebSocketCustomListener): void + ( + event: T, + listener: WebSocketCustomListener> + ): void } /** * Unregister event listener. */ off: WebSocketServerRaw['off'] & { - (event: string, listener: WebSocketCustomListener): void + (event: string, listener: Function): void } } diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index c38a4ac940ff6e..aacf1554b0de75 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -1,5 +1,16 @@ -// See https://stackoverflow.com/a/63549561. -export type CustomEventName = (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 keyof CustomEventMap ? CustomEventMap[T] : unknown diff --git a/packages/vite/types/hot.d.ts b/packages/vite/types/hot.d.ts index ee7c660056d086..f06846ff59d530 100644 --- a/packages/vite/types/hot.d.ts +++ b/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 @@ -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( + event: T, + cb: (payload: InferCustomEventPayload) => void + ): void + send(event: T, data?: InferCustomEventPayload): void } - -// See https://stackoverflow.com/a/63549561. -export type CustomEventName = (T extends `vite:${T}` - ? never - : T) & - (`vite:${T}` extends T ? never : T)