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
3 changes: 2 additions & 1 deletion docs/guide/api-hmr.md
Expand Up @@ -125,7 +125,7 @@ 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.
Calling `import.meta.hot.invalidate()` indicates that changes have occurred and the module's state should be treated as invalid. This will cause importers of the module to HMR, and thus provides a way to force changes to propagate upwards.
IanVS marked this conversation as resolved.
Show resolved Hide resolved

## `hot.on(event, cb)`

Expand All @@ -136,6 +136,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
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
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
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', (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
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