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: support import.meta.hot.invalidate #10244

Merged
merged 14 commits into from Sep 28, 2022
Merged
14 changes: 13 additions & 1 deletion docs/guide/api-hmr.md
Expand Up @@ -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)`

Expand All @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/client-types.d.ts
@@ -1,6 +1,7 @@
export type {
CustomEventMap,
InferCustomEventPayload
InferCustomEventPayload,
InvalidatePayload
} from './types/customEvent'
export type {
HMRPayload,
Expand Down
6 changes: 3 additions & 3 deletions packages/vite/src/client/client.ts
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion packages/vite/src/node/index.ts
Expand Up @@ -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,
Expand Down
16 changes: 15 additions & 1 deletion packages/vite/src/node/server/index.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/vite/src/types/customEvent.d.ts
Expand Up @@ -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<T extends string> =
Expand Down
6 changes: 5 additions & 1 deletion packages/vite/types/customEvent.d.ts
@@ -1 +1,5 @@
export type { CustomEventMap, InferCustomEventPayload } from '../client/types'
export type {
CustomEventMap,
InferCustomEventPayload,
InvalidatePayload
} from '../client/types'
24 changes: 22 additions & 2 deletions playground/hmr/__tests__/hmr.spec.ts
Expand Up @@ -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')

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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'))
Expand Down
5 changes: 5 additions & 0 deletions playground/hmr/hmr.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions playground/hmr/index.html
Expand Up @@ -20,6 +20,7 @@
<div class="nested"></div>
<div class="custom"></div>
<div class="virtual"></div>
<div class="invalidation"></div>
<div class="custom-communication"></div>
<div class="css-prev"></div>
<div class="css-post"></div>
Expand Down
9 changes: 9 additions & 0 deletions 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'
9 changes: 9 additions & 0 deletions 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