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 13 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
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', ownerPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sapphi is talking about this client-side event, I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this client event is even worth adding? What's the use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the use case for all of these is to help debug HMR problems and also to confirm they're working as expected in tests.

If I add the type to the HMRPayload union, then I'd need to handle it where I pointed out because of the exhaustive switch statement. I could create a separate type that's not included in that union, but it would be different from the others. But, I guess it is different, so maybe that's fine.

I'm also happy to remove the notifyListeners if that's preferred.

this.send('vite:invalidate', ownerPath)
},

// custom events
Expand Down
15 changes: 14 additions & 1 deletion packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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 +494,14 @@ export async function createServer(
handleFileAddUnlink(normalizePath(file), server)
})

ws.on('vite:invalidate', async (url: string) => {
const mod = moduleGraph.urlToModuleMap.get(url)
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
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', (event) => {
console.log(`>>> vite:invalidate -- ${event}`)
})

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