8000 fix(hmr): avoid infinite loop happening with `hot.invalidate` in circ… · vitejs/vite@d4ee5e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit d4ee5e8

Browse files
sapphi-redJSerFeng
andauthored
fix(hmr): avoid infinite loop happening with hot.invalidate in circular deps (#19870)
Co-authored-by: fengyu <1114550440@qq.com>
1 parent e051936 commit d4ee5e8

File tree

20 files changed

+186
-19
lines changed

20 files changed

+186
-19
lines changed

packages/vite/src/node/server/environment.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,16 @@ export class DevEnvironment extends BaseEnvironment {
134134
},
135135
})
136136

137-
this.hot.on('vite:invalidate', async ({ path, message }) => {
138-
invalidateModule(this, {
139-
path,
140-
message,
141-
})
142-
})
137+
this.hot.on(
138+
'vite:invalidate',
139+
async ({ path, message, firstInvalidatedBy }) => {
140+
invalidateModule(this, {
141+
path,
142+
message,
143+
F438 firstInvalidatedBy,
144+
})
145+
},
146+
)
143147

144148
const { optimizeDeps } = this.config
145149
if (context.depsOptimizer) {
@@ -277,6 +281,7 @@ function invalidateModule(
277281
m: {
278282
path: string
279283
message?: string
284+
firstInvalidatedBy: string
280285
},
281286
) {
282287
const mod = environment.moduleGraph.urlToModuleMap.get(m.path)
@@ -299,7 +304,7 @@ function invalidateModule(
299304
file,
300305
[...mod.importers],
301306
mod.lastHMRTimestamp,
302-
true,
307+
m.firstInvalidatedBy,
303308
)
304309
}
305310
}

packages/vite/src/node/server/hmr.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -625,14 +625,14 @@ export async function handleHMRUpdate(
625625
await hotUpdateEnvironments(server, hmr)
626626
}
627627

628-
type HasDeadEnd = boolean
628+
type HasDeadEnd = string | boolean
629629

630630
export function updateModules(
631631
environment: DevEnvironment,
632632
file: string,
633633
modules: EnvironmentModuleNode[],
634634
timestamp: number,
635-
afterInvalidation?: boolean,
635+
firstInvalidatedBy?: string,
636636
): void {
637637
const { hot } = environment
638638
const updates: Update[] = []
@@ -661,6 +661,19 @@ export function updateModules(
661661
continue
662662
}
663663

664+
// If import.meta.hot.invalidate was called already on that module for the same update,
665+
// it means any importer of that module can't hot update. We should fallback to full reload.
666+
if (
667+
firstInvalidatedBy &&
668+
boundaries.some(
669+
({ acceptedVia }) =>
670+
normalizeHmrUrl(acceptedVia.url) === firstInvalidatedBy,
671+
)
672+
) {
673+
needFullReload = 'circular import invalidate'
674+
continue
675+
}
676+
664677
updates.push(
665678
...boundaries.map(
666679
({ boundary, acceptedVia, isWithinCircularImport }) => ({
@@ -673,6 +686,7 @@ export function updateModules(
673686
? isExplicitImportRequired(acceptedVia.url)
674687
: false,
675688
isWithinCircularImport,
689+
firstInvalidatedBy,
676690
}),
677691
),
678692
)
@@ -685,7 +699,7 @@ export function updateModules(
685699
: ''
686700
environment.logger.info(
687701
colors.green(`page reload `) + colors.dim(file) + reason,
688-
{ clear: !afterInvalidation, timestamp: true },
702+
{ clear: !firstInvalidatedBy, timestamp: true },
689703
)
690704
hot.send({
691705
type: 'full-reload',
@@ -702,7 +716,7 @@ export function updateModules(
702716
environment.logger.info(
703717
colors.green(`hmr update `) +
704718
colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
705-
{ clear: !afterInvalidation, timestamp: true },
719+
{ clear: !firstInvalidatedBy, timestamp: true },
706720
)
707721
hot.send({
708722
type: 'update',

packages/vite/src/shared/hmr.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,17 @@ export class HMRContext implements ViteHotContext {
9797
decline(): void {}
9898

9999
invalidate(message: string): void {
100+
const firstInvalidatedBy =
101+
this.hmrClient.currentFirstInvalidatedBy ?? this.ownerPath
100102
this.hmrClient.notifyListeners('vite:invalidate', {
101103
path: this.ownerPath,
102104
message,
105+
firstInvalidatedBy,
103106
})
104107
this.send('vite:invalidate', {
105108
path: this.ownerPath,
106109
message,
110+
firstInvalidatedBy,
107111
})
108112
this.hmrClient.logger.debug(
109113
`invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`,
@@ -170,6 +174,7 @@ export class HMRClient {
170174
public dataMap = new Map<string, any>()
171175
public customListenersMap: CustomListenersMap = new Map()
172176
public ctxToListenersMap = new Map<string, CustomListenersMap>()
177+
public currentFirstInvalidatedBy: string | undefined
173178

174179
constructor(
175180
public logger: HMRLogger,
@@ -254,7 +259,7 @@ export class HMRClient {
254259
}
255260

256261
private async fetchUpdate(update: Update): Promise<(() => void) | undefined> {
257-
const { path, acceptedPath } = update
262+
const { path, acceptedPath, firstInvalidatedBy } = update
258263
const mod = this.hotModulesMap.get(path)
259264
if (!mod) {
260265
// In a code-splitting project,
@@ -282,13 +287,20 @@ export class HMRClient {
282287
}
283288

284289
return () => {
285-
for (const { deps, fn } of qualifiedCallbacks) {
286-
fn(
287-
deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)),
288-
)
290+
try {
291+
this.currentFirstInvalidatedBy = firstInvalidatedBy
292+
for (const { deps, fn } of qualifiedCallbacks) {
293+
fn(
294+
deps.map((dep) =>
295+
dep === acceptedPath ? fetchedModule : undefined,
296+
),
297+
)
298+
}
299+
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
300+
this.logger.debug(`hot updated: ${loggedPath}`)
301+
} finally {
302+
this.currentFirstInvalidatedBy = undefined
289303
}
290-
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
291-
this.logger.debug(`hot updated: ${loggedPath}`)
292304
}
293305
}
294306
}

packages/vite/types/customEvent.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface WebSocketConnectionPayload {
3030
export interface InvalidatePayload {
3131
path: string
3232
message: string | undefined
33+
firstInvalidatedBy: string
3334
}
3435

3536
/**

packages/vite/types/hmrPayload.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface Update {
3232
/** @internal */
3333
isWithinCircularImport?: boolean
3434
/** @internal */
35+
firstInvalidatedBy?: string
36+
/** @internal */
3537
invalidates?: string[]
3638
}
3739

playground/hmr-ssr/__tests__/hmr-ssr.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,26 @@ if (!isBuild) {
216216
)
217217
})
218218

219+
test('invalidate in circular dep should not trigger infinite HMR', async () => {
220+
const el = () => hmr('.invalidation-circular-deps')
221+
await untilUpdated(() => el(), 'child')
222+
editFile(
223+
'invalidation-circular-deps/circular-invalidate/child.js',
224+
(code) => code.replace('child', 'child updated'),
225+
)
226+
await untilUpdated(() => el(), 'child updated')
227+
})
228+
229+
test('invalidate in circular dep should be hot updated if possible', async () => {
230+
const el = () => hmr('.invalidation-circular-deps-handled')
231+
await untilUpdated(() => el(), 'child')
232+
editFile(
233+
'invalidation-circular-deps/invalidate-handled-in-circle/child.js',
234+
(code) => code.replace('child', 'child updated'),
235+
)
236+
await untilUpdated(() => el(), 'child updated')
237+
})
238+
219239
test('plugin hmr handler + custom event', async () => {
220240
const el = () => hmr('.custom')
221241
editFile('customFile.js', (code) => code.replace('custom', 'edited'))

p 10000 layground/hmr-ssr/hmr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { virtual } from 'virtual:file'
22
import { foo as depFoo, nestedFoo } from './hmrDep'
33
import './importing-updated'
4+
import './invalidation-circular-deps'
45
import './invalidation/parent'
56
import './file-delete-restore'
67
import './optional-chaining/parent'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import './parent'
2+
3+
if (import.meta.hot) {
4+
import.meta.hot.accept(() => {
5+
import.meta.hot.invalidate()
6+
})
7+
}
8+
9+
export const value = 'child'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { value } from './child'
2+
3+
if (import.meta.hot) {
4+
import.meta.hot.accept(() => {
5+
import.meta.hot.invalidate()
6+
})
7+
}
8+
9+
log('(invalidation circular deps) parent is executing')
10+
setTimeout(() => {
11+
globalThis.__HMR__['.invalidation-circular-deps'] = value
12+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import './circular-invalidate/parent'
2+
import './invalidate-handled-in-circle/parent'

0 commit comments

Comments
 (0)
0