diff --git a/docs/guide/api-hmr.md b/docs/guide/api-hmr.md index 3b416169f5231a..12a859370d2e11 100644 --- a/docs/guide/api-hmr.md +++ b/docs/guide/api-hmr.md @@ -125,7 +125,18 @@ Calling `import.meta.hot.decline()` indicates this module is not hot-updatable, ## `hot.invalidate()` -For now, calling `import.meta.hot.invalidate()` simply reloads the page. +A self-accepting module may realize during runtime that it can't handle a HMR update, and so the update needs to be forcefully propagated to importers. By calling `import.meta.hot.invalidate()`, the HMR server will invalidate the importers of the caller, as if the caller wasn't self-accepting. + +Note that you should always call `import.meta.hot.accept` even if you plan to call `invalidate` immediately afterwards, or else the HMR client won't listen for future changes to the self-accepting module. To communicate your intent clearly, we recommend calling `invalidate` within the `accept` callback like so: + + ```ts + import.meta.hot.accept(module => { + // You may use the new module instance to decide whether to invalidate. + if (cannotHandleUpdate(module)) { + import.meta.hot.invalidate() + } + }) + ``` ## `hot.on(event, cb)` @@ -136,6 +147,7 @@ The following HMR events are dispatched by Vite automatically: - `'vite:beforeUpdate'` when an update is about to be applied (e.g. a module will be replaced) - `'vite:beforeFullReload'` when a full reload is about to occur - `'vite:beforePrune'` when modules that are no longer needed are about to be pruned +- `'vite:invalidate'` when a module is invalidated with `import.meta.hot.invalidate()` - `'vite:error'` when an error occurs (e.g. syntax error) Custom HMR events can also be sent from plugins. See [handleHotUpdate](./api-plugin#handlehotupdate) for more details. diff --git a/packages/vite/src/client-types.d.ts b/packages/vite/src/client-types.d.ts index 270a1ac29f2d8b..cca320bc5d4e1b 100644 --- a/packages/vite/src/client-types.d.ts +++ b/packages/vite/src/client-types.d.ts @@ -1,6 +1,7 @@ export type { CustomEventMap, - InferCustomEventPayload + InferCustomEventPayload, + InvalidatePayload } from './types/customEvent' export type { HMRPayload, diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 83465d794358e2..3f974e77d9b52b 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -546,10 +546,10 @@ export function createHotContext(ownerPath: string): ViteHotContext { // eslint-disable-next-line @typescript-eslint/no-empty-function decline() {}, + // tell the server to re-perform hmr propagation from this module as root invalidate() { - // TODO should tell the server to re-perform hmr propagation - // from this module as root - location.reload() + notifyListeners('vite:invalidate', { path: ownerPath }) + this.send('vite:invalidate', { path: ownerPath }) }, // custom events diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 2bc81f001d6947..0e246615bfa5f3 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -102,7 +102,11 @@ export type { PrunePayload, ErrorPayload } from 'types/hmrPayload' -export type { CustomEventMap, InferCustomEventPayload } from 'types/customEvent' +export type { + CustomEventMap, + InferCustomEventPayload, + InvalidatePayload +} from 'types/customEvent' // [deprecated: use vite/client/types instead] export type { ImportGlobFunction, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 57a6a01e1fea12..77b91a4dfd6dba 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -13,6 +13,7 @@ import launchEditorMiddleware from 'launch-editor-middleware' import type { SourceMap } from 'rollup' import picomatch from 'picomatch' import type { Matcher } from 'picomatch' +import type { InvalidatePayload } from 'types/customEvent' import type { CommonServerOptions } from '../http' import { httpServerStart, @@ -67,7 +68,12 @@ import { timeMiddleware } from './middlewares/time' import { ModuleGraph } from './moduleGraph' import { errorMiddleware, prepareError } from './middlewares/error' import type { HmrOptions } from './hmr' -import { handleFileAddUnlink, handleHMRUpdate } from './hmr' +import { + getShortName, + handleFileAddUnlink, + handleHMRUpdate, + updateModules +} from './hmr' import { openBrowser } from './openBrowser' import type { TransformOptions, TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' @@ -489,6 +495,14 @@ export async function createServer( handleFileAddUnlink(normalizePath(file), server) }) + ws.on('vite:invalidate', async ({ path }: InvalidatePayload) => { + const mod = moduleGraph.urlToModuleMap.get(path) + if (mod && mod.isSelfAccepting && mod.lastHMRTimestamp > 0) { + const file = getShortName(mod.file!, config.root) + updateModules(file, [...mod.importers], mod.lastHMRTimestamp, server) + } + }) + if (!middlewareMode && httpServer) { httpServer.once('listening', () => { // update actual port since this may be different from initial value diff --git a/packages/vite/src/types/customEvent.d.ts b/packages/vite/src/types/customEvent.d.ts index af4db5d14fbe97..839e17dd729eda 100644 --- a/packages/vite/src/types/customEvent.d.ts +++ b/packages/vite/src/types/customEvent.d.ts @@ -10,6 +10,11 @@ export interface CustomEventMap { 'vite:beforePrune': PrunePayload 'vite:beforeFullReload': FullReloadPayload 'vite:error': ErrorPayload + 'vite:invalidate': InvalidatePayload +} + +export interface InvalidatePayload { + path: string } export type InferCustomEventPayload = diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 09fd7dc36ea481..d5bdbde98984fb 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -1 +1,5 @@ -export type { CustomEventMap, InferCustomEventPayload } from '../client/types' +export type { + CustomEventMap, + InferCustomEventPayload, + InvalidatePayload +} from '../client/types' diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index ef8def29a389a5..70d5a1b9ace52e 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -18,14 +18,14 @@ test('should render', async () => { if (!isBuild) { test('should connect', async () => { - expect(browserLogs.length).toBe(2) + expect(browserLogs.length).toBe(3) expect(browserLogs.some((msg) => msg.match('connected'))).toBe(true) browserLogs.length = 0 }) test('self accept', async () => { const el = await page.$('.app') - + browserLogs.length = 0 editFile('hmr.ts', (code) => code.replace('const foo = 1', 'const foo = 2')) await untilUpdated(() => el.textContent(), '2') @@ -91,6 +91,7 @@ if (!isBuild) { test('nested dep propagation', async () => { const el = await page.$('.nested') + browserLogs.length = 0 editFile('hmrNestedDep.js', (code) => code.replace('const foo = 1', 'const foo = 2') @@ -127,6 +128,25 @@ if (!isBuild) { browserLogs.length = 0 }) + test('invalidate', async () => { + browserLogs.length = 0 + const el = await page.$('.invalidation') + + editFile('invalidation/child.js', (code) => + code.replace('child', 'child updated') + ) + await untilUpdated(() => el.textContent(), 'child updated') + expect(browserLogs).toMatchObject([ + '>>> vite:beforeUpdate -- update', + '>>> vite:invalidate -- /invalidation/child.js', + '[vite] hot updated: /invalidation/child.js', + '>>> vite:beforeUpdate -- update', + '(invalidation) parent is executing', + '[vite] hot updated: /invalidation/parent.js' + ]) + browserLogs.length = 0 + }) + test('plugin hmr handler + custom event', async () => { const el = await page.$('.custom') editFile('customFile.js', (code) => code.replace('custom', 'edited')) diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index dc3c22eac9d56e..473dff9fdbfb88 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -2,6 +2,7 @@ import { virtual } from 'virtual:file' import { foo as depFoo, nestedFoo } from './hmrDep' import './importing-updated' +import './invalidation/parent' export const foo = 1 text('.app', foo) @@ -88,6 +89,10 @@ if (import.meta.hot) { console.log(`>>> vite:error -- ${event.type}`) }) + import.meta.hot.on('vite:invalidate', ({ path }) => { + console.log(`>>> vite:invalidate -- ${path}`) + }) + import.meta.hot.on('custom:foo', ({ msg }) => { text('.custom', msg) }) diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 28f08014036ade..b8d6065a9fd5e2 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -20,6 +20,7 @@
+
diff --git a/playground/hmr/invalidation/child.js b/playground/hmr/invalidation/child.js new file mode 100644 index 00000000000000..b424e2f83c3233 --- /dev/null +++ b/playground/hmr/invalidation/child.js @@ -0,0 +1,9 @@ +if (import.meta.hot) { + // Need to accept, to register a callback for HMR + import.meta.hot.accept(() => { + // Trigger HMR in importers + import.meta.hot.invalidate() + }) +} + +export const value = 'child' diff --git a/playground/hmr/invalidation/parent.js b/playground/hmr/invalidation/parent.js new file mode 100644 index 00000000000000..0b10298fff1aa4 --- /dev/null +++ b/playground/hmr/invalidation/parent.js @@ -0,0 +1,9 @@ +import { value } from './child' + +if (import.meta.hot) { + import.meta.hot.accept() +} + +console.log('(invalidation) parent is executing') + +document.querySelector('.invalidation').innerHTML = value