Skip to content

Commit

Permalink
feat: support import.meta.hot.invalidate (#10244)
Browse files Browse the repository at this point in the history
Co-authored-by: Alec Larson <[email protected]>
Co-authored-by: 翠 / green <[email protected]>
  • Loading branch information
3 people committed Sep 28, 2022
1 parent fe4dc8d commit fb8ab16
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 10 deletions.
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

0 comments on commit fb8ab16

Please sign in to comment.