Skip to content

Commit 0571b7c

Browse files
authored
refactor: make HMR agnostic to environment (#15179)
1 parent 56ef54e commit 0571b7c

File tree

3 files changed

+287
-217
lines changed

3 files changed

+287
-217
lines changed

packages/vite/src/client/client.ts

+27-217
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload'
2-
import type { ModuleNamespace, ViteHotContext } from 'types/hot'
1+
import type { ErrorPayload, HMRPayload } from 'types/hmrPayload'
2+
import type { ViteHotContext } from 'types/hot'
33
import type { InferCustomEventPayload } from 'types/customEvent'
4+
import { HMRClient, HMRContext } from '../shared/hmr'
45
import { ErrorOverlay, overlayId } from './overlay'
56
import '@vite/env'
67

@@ -110,17 +111,6 @@ function setupWebSocket(
110111
return socket
111112
}
112113

113-
function warnFailedFetch(err: Error, path: string | string[]) {
114-
if (!err.message.match('fetch')) {
115-
console.error(err)
116-
}
117-
console.error(
118-
`[hmr] Failed to reload ${path}. ` +
119-
`This could be due to syntax errors or importing non-existent ` +
120-
`modules. (see errors above)`,
121-
)
122-
}
123-
124114
function cleanUrl(pathname: string): string {
125115
const url = new URL(pathname, location.toString())
126116
url.searchParams.delete('direct')
@@ -144,6 +134,22 @@ const debounceReload = (time: number) => {
144134
}
145135
const pageReload = debounceReload(50)
146136

137+
const hmrClient = new HMRClient(console, async function importUpdatedModule({
138+
acceptedPath,
139+
timestamp,
140+
explicitImportRequired,
141+
}) {
142+
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
143+
return await import(
144+
/* @vite-ignore */
145+
base +
146+
acceptedPathWithoutQuery.slice(1) +
147+
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
148+
query ? `&${query}` : ''
149+
}`
150+
)
151+
})
152+
147153
async function handleMessage(payload: HMRPayload) {
148154
switch (payload.type) {
149155
case 'connected':
@@ -173,7 +179,7 @@ async function handleMessage(payload: HMRPayload) {
173179
await Promise.all(
174180
payload.updates.map(async (update): Promise<void> => {
175181
if (update.type === 'js-update') {
176-
return queueUpdate(fetchUpdate(update))
182+
return queueUpdate(hmrClient.fetchUpdate(update))
177183
}
178184

179185
// css-update
@@ -245,16 +251,7 @@ async function handleMessage(payload: HMRPayload) {
245251
break
246252
case 'prune':
247253
notifyListeners('vite:beforePrune', payload)
248-
// After an HMR update, some modules are no longer imported on the page
249-
// but they may have left behind side effects that need to be cleaned up
250-
// (.e.g style injections)
251-
// TODO Trigger their dispose callbacks.
252-
payload.paths.forEach((path) => {
253-
const fn = pruneMap.get(path)
254-
if (fn) {
255-
fn(dataMap.get(path))
256-
}
257-
})
254+
hmrClient.prunePaths(payload.paths)
258255
break
259256
case 'error': {
260257
notifyListeners('vite:error', payload)
@@ -280,10 +277,7 @@ function notifyListeners<T extends string>(
280277
data: InferCustomEventPayload<T>,
281278
): void
282279
function notifyListeners(event: string, data: any): void {
283-
const cbs = customListenersMap.get(event)
284-
if (cbs) {
285-
cbs.forEach((cb) => cb(data))
286-
}
280+
hmrClient.notifyListeners(event, data)
287281
}
288282

289283
const enableOverlay = __HMR_ENABLE_OVERLAY__
@@ -430,206 +424,22 @@ export function removeStyle(id: string): void {
430424
}
431425
}
432426

433-
async function fetchUpdate({
434-
path,
435-
acceptedPath,
436-
timestamp,
437-
explicitImportRequired,
438-
}: Update) {
439-
const mod = hotModulesMap.get(path)
440-
if (!mod) {
441-
// In a code-splitting project,
442-
// it is common that the hot-updating module is not loaded yet.
443-
// https://github.com/vitejs/vite/issues/721
444-
return
445-
}
446-
447-
let fetchedModule: ModuleNamespace | undefined
448-
const isSelfUpdate = path === acceptedPath
449-
450-
// determine the qualified callbacks before we re-import the modules
451-
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) =>
452-
deps.includes(acceptedPath),
453-
)
454-
455-
if (isSelfUpdate || qualifiedCallbacks.length > 0) {
456-
const disposer = disposeMap.get(acceptedPath)
457-
if (disposer) await disposer(dataMap.get(acceptedPath))
458-
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
459-
try {
460-
fetchedModule = await import(
461-
/* @vite-ignore */
462-
base +
463-
acceptedPathWithoutQuery.slice(1) +
464-
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
465-
query ? `&${query}` : ''
466-
}`
467-
)
468-
} catch (e) {
469-
warnFailedFetch(e, acceptedPath)
470-
}
471-
}
472-
473-
return () => {
474-
for (const { deps, fn } of qualifiedCallbacks) {
475-
fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)))
476-
}
477-
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
478-
console.debug(`[vite] hot updated: ${loggedPath}`)
479-
}
480-
}
481-
482427
function sendMessageBuffer() {
483428
if (socket.readyState === 1) {
484429
messageBuffer.forEach((msg) => socket.send(msg))
485430
messageBuffer.length = 0
486431
}
487432
}
488433

489-
interface HotModule {
490-
id: string
491-
callbacks: HotCallback[]
492-
}
493-
494-
interface HotCallback {
495-
// the dependencies must be fetchable paths
496-
deps: string[]
497-
fn: (modules: Array<ModuleNamespace | undefined>) => void
498-
}
499-
500-
type CustomListenersMap = Map<string, ((data: any) => void)[]>
501-
502-
const hotModulesMap = new Map<string, HotModule>()
503-
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
504-
const pruneMap = new Map<string, (data: any) => void | Promise<void>>()
505-
const dataMap = new Map<string, any>()
506-
const customListenersMap: CustomListenersMap = new Map()
507-
const ctxToListenersMap = new Map<string, CustomListenersMap>()
508-
509434
export function createHotContext(ownerPath: string): ViteHotContext {
510-
if (!dataMap.has(ownerPath)) {
511-
dataMap.set(ownerPath, {})
512-
}
513-
514-
// when a file is hot updated, a new context is created
515-
// clear its stale callbacks
516-
const mod = hotModulesMap.get(ownerPath)
517-
if (mod) {
518-
mod.callbacks = []
519-
}
520-
521-
// clear stale custom event listeners
522-
const staleListeners = ctxToListenersMap.get(ownerPath)
523-
if (staleListeners) {
524-
for (const [event, staleFns] of staleListeners) {
525-
const listeners = customListenersMap.get(event)
526-
if (listeners) {
527-
customListenersMap.set(
528-
event,
529-
listeners.filter((l) => !staleFns.includes(l)),
530-
)
531-
}
532-
}
533-
}
534-
535-
const newListeners: CustomListenersMap = new Map()
536-
ctxToListenersMap.set(ownerPath, newListeners)
537-
538-
function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
539-
const mod: HotModule = hotModulesMap.get(ownerPath) || {
540-
id: ownerPath,
541-
callbacks: [],
542-
}
543-
mod.callbacks.push({
544-
deps,
545-
fn: callback,
546-
})
547-
hotModulesMap.set(ownerPath, mod)
548-
}
549-
550-
const hot: ViteHotContext = {
551-
get data() {
552-
return dataMap.get(ownerPath)
553-
},
554-
555-
accept(deps?: any, callback?: any) {
556-
if (typeof deps === 'function' || !deps) {
557-
// self-accept: hot.accept(() => {})
558-
acceptDeps([ownerPath], ([mod]) => deps?.(mod))
559-
} else if (typeof deps === 'string') {
560-
// explicit deps
561-
acceptDeps([deps], ([mod]) => callback?.(mod))
562-
} else if (Array.isArray(deps)) {
563-
acceptDeps(deps, callback)
564-
} else {
565-
throw new Error(`invalid hot.accept() usage.`)
566-
}
567-
},
568-
569-
// export names (first arg) are irrelevant on the client side, they're
570-
// extracted in the server for propagation
571-
acceptExports(_, callback) {
572-
acceptDeps([ownerPath], ([mod]) => callback?.(mod))
435+
return new HMRContext(ownerPath, hmrClient, {
436+
addBuffer(message) {
437+
messageBuffer.push(message)
573438
},
574-
575-
dispose(cb) {
576-
disposeMap.set(ownerPath, cb)
577-
},
578-
579-
prune(cb) {
580-
pruneMap.set(ownerPath, cb)
581-
},
582-
583-
// Kept for backward compatibility (#11036)
584-
// @ts-expect-error untyped
585-
// eslint-disable-next-line @typescript-eslint/no-empty-function
586-
decline() {},
587-
588-
// tell the server to re-perform hmr propagation from this module as root
589-
invalidate(message) {
590-
notifyListeners('vite:invalidate', { path: ownerPath, message })
591-
this.send('vite:invalidate', { path: ownerPath, message })
592-
console.debug(
593-
`[vite] invalidate ${ownerPath}${message ? `: ${message}` : ''}`,
594-
)
595-
},
596-
597-
// custom events
598-
on(event, cb) {
599-
const addToMap = (map: Map<string, any[]>) => {
600-
const existing = map.get(event) || []
601-
existing.push(cb)
602-
map.set(event, existing)
603-
}
604-
addToMap(customListenersMap)
605-
addToMap(newListeners)
606-
},
607-
608-
// remove a custom event
609-
off(event, cb) {
610-
const removeFromMap = (map: Map<string, any[]>) => {
611-
const existing = map.get(event)
612-
if (existing === undefined) {
613-
return
614-
}
615-
const pruned = existing.filter((l) => l !== cb)
616-
if (pruned.length === 0) {
617-
map.delete(event)
618-
return
619-
}
620-
map.set(event, pruned)
621-
}
622-
removeFromMap(customListenersMap)
623-
removeFromMap(newListeners)
624-
},
625-
626-
send(event, data) {
627-
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
439+
send() {
628440
sendMessageBuffer()
629441
},
630-
}
631-
632-
return hot
442+
})
633443
}
634444

635445
/**

0 commit comments

Comments
 (0)