From 274c709fa42f4c0032014a5468575ea2ef000b9a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 29 Nov 2023 15:41:13 +0100 Subject: [PATCH 1/5] refactor: move hmr to "shared" folder --- packages/vite/src/client/client.ts | 237 +++---------------------- packages/vite/src/shared/hmr.ts | 237 +++++++++++++++++++++++++ packages/vite/src/shared/tsconfig.json | 9 + 3 files changed, 274 insertions(+), 209 deletions(-) create mode 100644 packages/vite/src/shared/hmr.ts create mode 100644 packages/vite/src/shared/tsconfig.json diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 86c1bd0a837a47..81cf247cf750b1 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,6 +1,7 @@ -import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload' -import type { ModuleNamespace, ViteHotContext } from 'types/hot' +import type { ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' +import { ClientHMR, ClientHMRContext } from '../shared/hmr' import { ErrorOverlay, overlayId } from './overlay' import '@vite/env' @@ -110,17 +111,6 @@ function setupWebSocket( return socket } -function warnFailedFetch(err: Error, path: string | string[]) { - if (!err.message.match('fetch')) { - console.error(err) - } - console.error( - `[hmr] Failed to reload ${path}. ` + - `This could be due to syntax errors or importing non-existent ` + - `modules. (see errors above)`, - ) -} - function cleanUrl(pathname: string): string { const url = new URL(pathname, location.toString()) url.searchParams.delete('direct') @@ -144,6 +134,22 @@ const debounceReload = (time: number) => { } const pageReload = debounceReload(50) +const hmr = new ClientHMR(console, async function importUpdatedModule({ + acceptedPath, + timestamp, + explicitImportRequired, +}) { + const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) + return await import( + /* @vite-ignore */ + base + + acceptedPathWithoutQuery.slice(1) + + `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ + query ? `&${query}` : '' + }` + ) +}) + async function handleMessage(payload: HMRPayload) { switch (payload.type) { case 'connected': @@ -173,7 +179,7 @@ async function handleMessage(payload: HMRPayload) { await Promise.all( payload.updates.map(async (update): Promise => { if (update.type === 'js-update') { - return queueUpdate(fetchUpdate(update)) + return queueUpdate(hmr.fetchUpdate(update)) } // css-update @@ -250,9 +256,9 @@ async function handleMessage(payload: HMRPayload) { // (.e.g style injections) // TODO Trigger their dispose callbacks. payload.paths.forEach((path) => { - const fn = pruneMap.get(path) + const fn = hmr.pruneMap.get(path) if (fn) { - fn(dataMap.get(path)) + fn(hmr.dataMap.get(path)) } }) break @@ -280,10 +286,7 @@ function notifyListeners( data: InferCustomEventPayload, ): void function notifyListeners(event: string, data: any): void { - const cbs = customListenersMap.get(event) - if (cbs) { - cbs.forEach((cb) => cb(data)) - } + hmr.notifyListeners(event, data) } const enableOverlay = __HMR_ENABLE_OVERLAY__ @@ -430,55 +433,6 @@ export function removeStyle(id: string): void { } } -async function fetchUpdate({ - path, - acceptedPath, - timestamp, - explicitImportRequired, -}: Update) { - const mod = hotModulesMap.get(path) - if (!mod) { - // In a code-splitting project, - // it is common that the hot-updating module is not loaded yet. - // https://github.com/vitejs/vite/issues/721 - return - } - - let fetchedModule: ModuleNamespace | undefined - const isSelfUpdate = path === acceptedPath - - // determine the qualified callbacks before we re-import the modules - const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => - deps.includes(acceptedPath), - ) - - if (isSelfUpdate || qualifiedCallbacks.length > 0) { - const disposer = disposeMap.get(acceptedPath) - if (disposer) await disposer(dataMap.get(acceptedPath)) - const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) - try { - fetchedModule = await import( - /* @vite-ignore */ - base + - acceptedPathWithoutQuery.slice(1) + - `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ - query ? `&${query}` : '' - }` - ) - } catch (e) { - warnFailedFetch(e, acceptedPath) - } - } - - return () => { - for (const { deps, fn } of qualifiedCallbacks) { - fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined))) - } - const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}` - console.debug(`[vite] hot updated: ${loggedPath}`) - } -} - function sendMessageBuffer() { if (socket.readyState === 1) { messageBuffer.forEach((msg) => socket.send(msg)) @@ -486,150 +440,15 @@ function sendMessageBuffer() { } } -interface HotModule { - id: string - callbacks: HotCallback[] -} - -interface HotCallback { - // the dependencies must be fetchable paths - deps: string[] - fn: (modules: Array) => void -} - -type CustomListenersMap = Map void)[]> - -const hotModulesMap = new Map() -const disposeMap = new Map void | Promise>() -const pruneMap = new Map void | Promise>() -const dataMap = new Map() -const customListenersMap: CustomListenersMap = new Map() -const ctxToListenersMap = new Map() - export function createHotContext(ownerPath: string): ViteHotContext { - if (!dataMap.has(ownerPath)) { - dataMap.set(ownerPath, {}) - } - - // when a file is hot updated, a new context is created - // clear its stale callbacks - const mod = hotModulesMap.get(ownerPath) - if (mod) { - mod.callbacks = [] - } - - // clear stale custom event listeners - const staleListeners = ctxToListenersMap.get(ownerPath) - if (staleListeners) { - for (const [event, staleFns] of staleListeners) { - const listeners = customListenersMap.get(event) - if (listeners) { - customListenersMap.set( - event, - listeners.filter((l) => !staleFns.includes(l)), - ) - } - } - } - - const newListeners: CustomListenersMap = new Map() - ctxToListenersMap.set(ownerPath, newListeners) - - function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) { - const mod: HotModule = hotModulesMap.get(ownerPath) || { - id: ownerPath, - callbacks: [], - } - mod.callbacks.push({ - deps, - fn: callback, - }) - hotModulesMap.set(ownerPath, mod) - } - - const hot: ViteHotContext = { - get data() { - return dataMap.get(ownerPath) + return new ClientHMRContext(ownerPath, hmr, { + addBuffer(message) { + messageBuffer.push(message) }, - - accept(deps?: any, callback?: any) { - if (typeof deps === 'function' || !deps) { - // self-accept: hot.accept(() => {}) - acceptDeps([ownerPath], ([mod]) => deps?.(mod)) - } else if (typeof deps === 'string') { - // explicit deps - acceptDeps([deps], ([mod]) => callback?.(mod)) - } else if (Array.isArray(deps)) { - acceptDeps(deps, callback) - } else { - throw new Error(`invalid hot.accept() usage.`) - } - }, - - // export names (first arg) are irrelevant on the client side, they're - // extracted in the server for propagation - acceptExports(_, callback) { - acceptDeps([ownerPath], ([mod]) => callback?.(mod)) - }, - - dispose(cb) { - disposeMap.set(ownerPath, cb) - }, - - prune(cb) { - pruneMap.set(ownerPath, cb) - }, - - // Kept for backward compatibility (#11036) - // @ts-expect-error untyped - // eslint-disable-next-line @typescript-eslint/no-empty-function - decline() {}, - - // tell the server to re-perform hmr propagation from this module as root - invalidate(message) { - notifyListeners('vite:invalidate', { path: ownerPath, message }) - this.send('vite:invalidate', { path: ownerPath, message }) - console.debug( - `[vite] invalidate ${ownerPath}${message ? `: ${message}` : ''}`, - ) - }, - - // custom events - on(event, cb) { - const addToMap = (map: Map) => { - const existing = map.get(event) || [] - existing.push(cb) - map.set(event, existing) - } - addToMap(customListenersMap) - addToMap(newListeners) - }, - - // remove a custom event - off(event, cb) { - const removeFromMap = (map: Map) => { - const existing = map.get(event) - if (existing === undefined) { - return - } - const pruned = existing.filter((l) => l !== cb) - if (pruned.length === 0) { - map.delete(event) - return - } - map.set(event, pruned) - } - removeFromMap(customListenersMap) - removeFromMap(newListeners) - }, - - send(event, data) { - messageBuffer.push(JSON.stringify({ type: 'custom', event, data })) + send() { sendMessageBuffer() }, - } - - return hot + }) } /** diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts new file mode 100644 index 00000000000000..32d8ad7b67f6d3 --- /dev/null +++ b/packages/vite/src/shared/hmr.ts @@ -0,0 +1,237 @@ +import type { Update } from 'types/hmrPayload' +import type { ModuleNamespace, ViteHotContext } from 'types/hot' +import type { InferCustomEventPayload } from 'types/customEvent' + +type CustomListenersMap = Map void)[]> + +interface HotModule { + id: string + callbacks: HotCallback[] +} + +interface HotCallback { + // the dependencies must be fetchable paths + deps: string[] + fn: (modules: Array) => void +} + +interface Connection { + addBuffer(message: string): void + send(): unknown +} + +export class ClientHMRContext implements ViteHotContext { + private newListeners: CustomListenersMap + + constructor( + private ownerPath: string, + private hmr: ClientHMR, + private connection: Connection, + ) { + if (!hmr.dataMap.has(ownerPath)) { + hmr.dataMap.set(ownerPath, {}) + } + + // when a file is hot updated, a new context is created + // clear its stale callbacks + const mod = hmr.hotModulesMap.get(ownerPath) + if (mod) { + mod.callbacks = [] + } + + // clear stale custom event listeners + const staleListeners = hmr.ctxToListenersMap.get(ownerPath) + if (staleListeners) { + for (const [event, staleFns] of staleListeners) { + const listeners = hmr.customListenersMap.get(event) + if (listeners) { + hmr.customListenersMap.set( + event, + listeners.filter((l) => !staleFns.includes(l)), + ) + } + } + } + + this.newListeners = new Map() + hmr.ctxToListenersMap.set(ownerPath, this.newListeners) + } + + get data(): any { + return this.hmr.dataMap.get(this.ownerPath) + } + + accept(deps?: any, callback?: any): void { + if (typeof deps === 'function' || !deps) { + // self-accept: hot.accept(() => {}) + this.acceptDeps([this.ownerPath], ([mod]) => deps?.(mod)) + } else if (typeof deps === 'string') { + // explicit deps + this.acceptDeps([deps], ([mod]) => callback?.(mod)) + } else if (Array.isArray(deps)) { + this.acceptDeps(deps, callback) + } else { + throw new Error(`invalid hot.accept() usage.`) + } + } + + // export names (first arg) are irrelevant on the client side, they're + // extracted in the server for propagation + acceptExports( + _: string | readonly string[], + callback: (data: any) => void, + ): void { + this.acceptDeps([this.ownerPath], ([mod]) => callback?.(mod)) + } + + dispose(cb: (data: any) => void): void { + this.hmr.disposeMap.set(this.ownerPath, cb) + } + + prune(cb: (data: any) => void): void { + this.hmr.pruneMap.set(this.ownerPath, cb) + } + + // Kept for backward compatibility (#11036) + // eslint-disable-next-line @typescript-eslint/no-empty-function + decline(): void {} + + invalidate(message: string): void { + this.hmr.notifyListeners('vite:invalidate', { + path: this.ownerPath, + message, + }) + this.send('vite:invalidate', { path: this.ownerPath, message }) + this.hmr.logger.debug( + `[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, + ) + } + + on( + event: T, + cb: (payload: InferCustomEventPayload) => void, + ): void { + const addToMap = (map: Map) => { + const existing = map.get(event) || [] + existing.push(cb) + map.set(event, existing) + } + addToMap(this.hmr.customListenersMap) + addToMap(this.newListeners) + } + + off( + event: T, + cb: (payload: InferCustomEventPayload) => void, + ): void { + const removeFromMap = (map: Map) => { + const existing = map.get(event) + if (existing === undefined) { + return + } + const pruned = existing.filter((l) => l !== cb) + if (pruned.length === 0) { + map.delete(event) + return + } + map.set(event, pruned) + } + removeFromMap(this.hmr.customListenersMap) + removeFromMap(this.newListeners) + } + + send(event: T, data?: InferCustomEventPayload): void { + this.connection.addBuffer(JSON.stringify({ type: 'custom', event, data })) + this.connection.send() + } + + private acceptDeps( + deps: string[], + callback: HotCallback['fn'] = () => {}, + ): void { + const mod: HotModule = this.hmr.hotModulesMap.get(this.ownerPath) || { + id: this.ownerPath, + callbacks: [], + } + mod.callbacks.push({ + deps, + fn: callback, + }) + this.hmr.hotModulesMap.set(this.ownerPath, mod) + } +} + +export class ClientHMR { + public hotModulesMap = new Map() + public disposeMap = new Map void | Promise>() + public pruneMap = new Map void | Promise>() + public dataMap = new Map() + public customListenersMap: CustomListenersMap = new Map() + public ctxToListenersMap = new Map() + + constructor( + public logger: Console, + private importUpdatedModule: (update: Update) => Promise, + ) {} + + public notifyListeners( + event: T, + data: InferCustomEventPayload, + ): void + public notifyListeners(event: string, data: any): void { + const cbs = this.customListenersMap.get(event) + if (cbs) { + cbs.forEach((cb) => cb(data)) + } + } + + protected warnFailedUpdate(err: Error, path: string | string[]): void { + if (!err.message.match('fetch')) { + this.logger.error(err) + } + this.logger.error( + `[hmr] Failed to reload ${path}. ` + + `This could be due to syntax errors or importing non-existent ` + + `modules. (see errors above)`, + ) + } + + public async fetchUpdate(update: Update): Promise<(() => void) | undefined> { + const { path, acceptedPath } = update + const mod = this.hotModulesMap.get(path) + if (!mod) { + // In a code-splitting project, + // it is common that the hot-updating module is not loaded yet. + // https://github.com/vitejs/vite/issues/721 + return + } + + let fetchedModule: ModuleNamespace | undefined + const isSelfUpdate = path === acceptedPath + + // determine the qualified callbacks before we re-import the modules + const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => + deps.includes(acceptedPath), + ) + + if (isSelfUpdate || qualifiedCallbacks.length > 0) { + const disposer = this.disposeMap.get(acceptedPath) + if (disposer) await disposer(this.dataMap.get(acceptedPath)) + try { + fetchedModule = await this.importUpdatedModule(update) + } catch (e) { + this.warnFailedUpdate(e, acceptedPath) + } + } + + return () => { + for (const { deps, fn } of qualifiedCallbacks) { + fn( + deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)), + ) + } + const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}` + this.logger.debug(`[vite] hot updated: ${loggedPath}`) + } + } +} diff --git a/packages/vite/src/shared/tsconfig.json b/packages/vite/src/shared/tsconfig.json new file mode 100644 index 00000000000000..a7f7890f1d0e7b --- /dev/null +++ b/packages/vite/src/shared/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["./", "../dep-types", "../types"], + "exclude": ["**/__tests__"], + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "stripInternal": true + } +} From 6eeb8445b45cbdbdcb41013e6c69a9b302aa3af1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 29 Nov 2023 17:29:43 +0100 Subject: [PATCH 2/5] refactor: renaming HMR client and context to be inline with other names --- packages/vite/src/client/client.ts | 14 +++++------ packages/vite/src/shared/hmr.ts | 39 +++++++++++++++--------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 81cf247cf750b1..16ad040a7d652b 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,7 +1,7 @@ import type { ErrorPayload, HMRPayload } from 'types/hmrPayload' import type { ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' -import { ClientHMR, ClientHMRContext } from '../shared/hmr' +import { HMRClient, HMRContext } from '../shared/hmr' import { ErrorOverlay, overlayId } from './overlay' import '@vite/env' @@ -134,7 +134,7 @@ const debounceReload = (time: number) => { } const pageReload = debounceReload(50) -const hmr = new ClientHMR(console, async function importUpdatedModule({ +const hmrClient = new HMRClient(console, async function importUpdatedModule({ acceptedPath, timestamp, explicitImportRequired, @@ -179,7 +179,7 @@ async function handleMessage(payload: HMRPayload) { await Promise.all( payload.updates.map(async (update): Promise => { if (update.type === 'js-update') { - return queueUpdate(hmr.fetchUpdate(update)) + return queueUpdate(hmrClient.fetchUpdate(update)) } // css-update @@ -256,9 +256,9 @@ async function handleMessage(payload: HMRPayload) { // (.e.g style injections) // TODO Trigger their dispose callbacks. payload.paths.forEach((path) => { - const fn = hmr.pruneMap.get(path) + const fn = hmrClient.pruneMap.get(path) if (fn) { - fn(hmr.dataMap.get(path)) + fn(hmrClient.dataMap.get(path)) } }) break @@ -286,7 +286,7 @@ function notifyListeners( data: InferCustomEventPayload, ): void function notifyListeners(event: string, data: any): void { - hmr.notifyListeners(event, data) + hmrClient.notifyListeners(event, data) } const enableOverlay = __HMR_ENABLE_OVERLAY__ @@ -441,7 +441,7 @@ function sendMessageBuffer() { } export function createHotContext(ownerPath: string): ViteHotContext { - return new ClientHMRContext(ownerPath, hmr, { + return new HMRContext(ownerPath, hmrClient, { addBuffer(message) { messageBuffer.push(message) }, diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 32d8ad7b67f6d3..3a5d4e33c7127a 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -20,32 +20,32 @@ interface Connection { send(): unknown } -export class ClientHMRContext implements ViteHotContext { +export class HMRContext implements ViteHotContext { private newListeners: CustomListenersMap constructor( private ownerPath: string, - private hmr: ClientHMR, + private hmrClient: HMRClient, private connection: Connection, ) { - if (!hmr.dataMap.has(ownerPath)) { - hmr.dataMap.set(ownerPath, {}) + if (!hmrClient.dataMap.has(ownerPath)) { + hmrClient.dataMap.set(ownerPath, {}) } // when a file is hot updated, a new context is created // clear its stale callbacks - const mod = hmr.hotModulesMap.get(ownerPath) + const mod = hmrClient.hotModulesMap.get(ownerPath) if (mod) { mod.callbacks = [] } // clear stale custom event listeners - const staleListeners = hmr.ctxToListenersMap.get(ownerPath) + const staleListeners = hmrClient.ctxToListenersMap.get(ownerPath) if (staleListeners) { for (const [event, staleFns] of staleListeners) { - const listeners = hmr.customListenersMap.get(event) + const listeners = hmrClient.customListenersMap.get(event) if (listeners) { - hmr.customListenersMap.set( + hmrClient.customListenersMap.set( event, listeners.filter((l) => !staleFns.includes(l)), ) @@ -54,11 +54,11 @@ export class ClientHMRContext implements ViteHotContext { } this.newListeners = new Map() - hmr.ctxToListenersMap.set(ownerPath, this.newListeners) + hmrClient.ctxToListenersMap.set(ownerPath, this.newListeners) } get data(): any { - return this.hmr.dataMap.get(this.ownerPath) + return this.hmrClient.dataMap.get(this.ownerPath) } accept(deps?: any, callback?: any): void { @@ -85,11 +85,11 @@ export class ClientHMRContext implements ViteHotContext { } dispose(cb: (data: any) => void): void { - this.hmr.disposeMap.set(this.ownerPath, cb) + this.hmrClient.disposeMap.set(this.ownerPath, cb) } prune(cb: (data: any) => void): void { - this.hmr.pruneMap.set(this.ownerPath, cb) + this.hmrClient.pruneMap.set(this.ownerPath, cb) } // Kept for backward compatibility (#11036) @@ -97,12 +97,12 @@ export class ClientHMRContext implements ViteHotContext { decline(): void {} invalidate(message: string): void { - this.hmr.notifyListeners('vite:invalidate', { + this.hmrClient.notifyListeners('vite:invalidate', { path: this.ownerPath, message, }) this.send('vite:invalidate', { path: this.ownerPath, message }) - this.hmr.logger.debug( + this.hmrClient.logger.debug( `[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, ) } @@ -116,7 +116,7 @@ export class ClientHMRContext implements ViteHotContext { existing.push(cb) map.set(event, existing) } - addToMap(this.hmr.customListenersMap) + addToMap(this.hmrClient.customListenersMap) addToMap(this.newListeners) } @@ -136,7 +136,7 @@ export class ClientHMRContext implements ViteHotContext { } map.set(event, pruned) } - removeFromMap(this.hmr.customListenersMap) + removeFromMap(this.hmrClient.customListenersMap) removeFromMap(this.newListeners) } @@ -149,7 +149,7 @@ export class ClientHMRContext implements ViteHotContext { deps: string[], callback: HotCallback['fn'] = () => {}, ): void { - const mod: HotModule = this.hmr.hotModulesMap.get(this.ownerPath) || { + const mod: HotModule = this.hmrClient.hotModulesMap.get(this.ownerPath) || { id: this.ownerPath, callbacks: [], } @@ -157,11 +157,11 @@ export class ClientHMRContext implements ViteHotContext { deps, fn: callback, }) - this.hmr.hotModulesMap.set(this.ownerPath, mod) + this.hmrClient.hotModulesMap.set(this.ownerPath, mod) } } -export class ClientHMR { +export class HMRClient { public hotModulesMap = new Map() public disposeMap = new Map void | Promise>() public pruneMap = new Map void | Promise>() @@ -171,6 +171,7 @@ export class ClientHMR { constructor( public logger: Console, + // this allows up to implement reloading via different methods depending on the environment private importUpdatedModule: (update: Update) => Promise, ) {} From 601df1fbbfb5cbc64dae18b99acf8ef3fffbf373 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 30 Nov 2023 14:05:23 +0100 Subject: [PATCH 3/5] refactor: move prunePaths into its own method --- packages/vite/src/client/client.ts | 11 +---------- packages/vite/src/shared/hmr.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 16ad040a7d652b..41dcebc5e27a3c 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -251,16 +251,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) - // TODO Trigger their dispose callbacks. - payload.paths.forEach((path) => { - const fn = hmrClient.pruneMap.get(path) - if (fn) { - fn(hmrClient.dataMap.get(path)) - } - }) + hmrClient.prunePaths(payload.paths) break case 'error': { notifyListeners('vite:error', payload) diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 3a5d4e33c7127a..c8521e23cc1ca0 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -186,6 +186,19 @@ export class HMRClient { } } + // 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) + // TODO Trigger their dispose callbacks. + public prunePaths(paths: string[]): void { + paths.forEach((path) => { + const fn = this.pruneMap.get(path) + if (fn) { + fn(this.dataMap.get(path)) + } + }) + } + protected warnFailedUpdate(err: Error, path: string | string[]): void { if (!err.message.match('fetch')) { this.logger.error(err) From 3821aa461c3758655d80c47739ee45c361f43d82 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 30 Nov 2023 14:08:16 +0100 Subject: [PATCH 4/5] chore: return uknown from notifyListeners to allow waiting on cb callbacks if needed This is needed when closing a server for example (need to wait!) --- packages/vite/src/shared/hmr.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index c8521e23cc1ca0..34deaa658d8b3a 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -178,11 +178,11 @@ export class HMRClient { public notifyListeners( event: T, data: InferCustomEventPayload, - ): void - public notifyListeners(event: string, data: any): void { + ): unknown + public notifyListeners(event: string, data: any): unknown { const cbs = this.customListenersMap.get(event) if (cbs) { - cbs.forEach((cb) => cb(data)) + return cbs.map((cb) => cb(data)) } } From bb57de22d7236e1fc54de376229815011932313d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 30 Nov 2023 16:15:55 +0100 Subject: [PATCH 5/5] chore: improve notifyListeners --- packages/vite/src/shared/hmr.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 34deaa658d8b3a..60c26b50e49da5 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -175,14 +175,14 @@ export class HMRClient { private importUpdatedModule: (update: Update) => Promise, ) {} - public notifyListeners( + public async notifyListeners( event: T, data: InferCustomEventPayload, - ): unknown - public notifyListeners(event: string, data: any): unknown { + ): Promise + public async notifyListeners(event: string, data: any): Promise { const cbs = this.customListenersMap.get(event) if (cbs) { - return cbs.map((cb) => cb(data)) + await Promise.allSettled(cbs.map((cb) => cb(data))) } }