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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/guide/api-hmr.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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