Skip to content

Commit 57463fc

Browse files
authored
fix(runtime): runtime HMR affects only imported files (#15898)
1 parent 37af8a7 commit 57463fc

File tree

8 files changed

+139
-13
lines changed

8 files changed

+139
-13
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export async function handleHMRUpdate(
166166
hot.send({
167167
type: 'full-reload',
168168
path: '*',
169+
triggeredBy: path.resolve(config.root, file),
169170
})
170171
return
171172
}
@@ -272,6 +273,7 @@ export function updateModules(
272273
)
273274
hot.send({
274275
type: 'full-reload',
276+
triggeredBy: path.resolve(config.root, file),
275277
})
276278
return
277279
}
@@ -295,7 +297,7 @@ export function updateModules(
295297
function populateSSRImporters(
296298
module: ModuleNode,
297299
timestamp: number,
298-
seen: Set<ModuleNode>,
300+
seen: Set<ModuleNode> = new Set(),
299301
) {
300302
module.ssrImportedModules.forEach((importer) => {
301303
if (seen.has(importer)) {
@@ -313,9 +315,9 @@ function populateSSRImporters(
313315
}
314316

315317
function getSSRInvalidatedImporters(module: ModuleNode) {
316-
return [
317-
...populateSSRImporters(module, module.lastHMRTimestamp, new Set()),
318-
].map((m) => m.file!)
318+
return [...populateSSRImporters(module, module.lastHMRTimestamp)].map(
319+
(m) => m.file!,
320+
)
319321
}
320322

321323
export async function handleFileAddUnlink(

packages/vite/src/node/ssr/runtime/hmrHandler.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,28 @@ export async function handleHMRPayload(
4343
await hmrClient.notifyListeners(payload.event, payload.data)
4444
break
4545
}
46-
case 'full-reload':
46+
case 'full-reload': {
47+
const { triggeredBy } = payload
48+
const clearEntrypoints = triggeredBy
49+
? [...runtime.entrypoints].filter((entrypoint) =>
50+
runtime.moduleCache.isImported({
51+
importedId: triggeredBy,
52+
importedBy: entrypoint,
53+
}),
54+
)
55+
: [...runtime.entrypoints]
56+
57+
if (!clearEntrypoints.length) break
58+
4759
hmrClient.logger.debug(`[vite] program reload`)
4860
await hmrClient.notifyListeners('vite:beforeFullReload', payload)
49-
Array.from(runtime.moduleCache.keys()).forEach((id) => {
50-
if (!id.includes('node_modules')) {
51-
runtime.moduleCache.deleteByModuleId(id)
52-
}
53-
})
54-
for (const id of runtime.entrypoints) {
61+
runtime.moduleCache.clear()
62+
63+
for (const id of clearEntrypoints) {
5564
await runtime.executeUrl(id)
5665
}
5766
break
67+
}
5868
case 'prune':
5969
await hmrClient.notifyListeners('vite:beforePrune', payload)
6070
hmrClient.prunePaths(payload.paths)

packages/vite/src/node/ssr/runtime/moduleCache.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,57 @@ export class ModuleCacheMap extends Map<string, ModuleCache> {
6565
return this.deleteByModuleId(this.normalize(fsPath))
6666
}
6767

68+
invalidate(id: string): void {
69+
const module = this.get(id)
70+
module.evaluated = false
71+
module.meta = undefined
72+
module.map = undefined
73+
module.promise = undefined
74+
module.exports = undefined
75+
// remove imports in case they are changed,
76+
// don't remove the importers because otherwise it will be empty after evaluation
77+
// this can create a bug when file was removed but it still triggers full-reload
78+
// we are fine with the bug for now because it's not a common case
79+
module.imports?.clear()
80+
}
81+
82+
isImported(
83+
{
84+
importedId,
85+
importedBy,
86+
}: {
87+
importedId: string
88+
importedBy: string
89+
},
90+
seen = new Set<string>(),
91+
): boolean {
92+
importedId = this.normalize(importedId)
93+
importedBy = this.normalize(importedBy)
94+
95+
if (importedBy === importedId) return true
96+
97+
if (seen.has(importedId)) return false
98+
seen.add(importedId)
99+
100+
const fileModule = this.getByModuleId(importedId)
101+
const importers = fileModule?.importers
102+
103+
if (!importers) return false
104+
105+
if (importers.has(importedBy)) return true
106+
107+
for (const importer of importers) {
108+
if (
109+
this.isImported({
110+
importedBy: importedBy,
111+
importedId: importer,
112+
})
113+
)
114+
return true
115+
}
116+
return false
117+
}
118+
68119
/**
69120
* Invalidate modules that dependent on the given modules, up to the main entry
70121
*/

packages/vite/src/node/ssr/runtime/runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export class ViteRuntime {
7272
: options.hmr.logger || console,
7373
options.hmr.connection,
7474
({ acceptedPath, ssrInvalidates }) => {
75-
this.moduleCache.delete(acceptedPath)
75+
this.moduleCache.invalidate(acceptedPath)
7676
if (ssrInvalidates) {
7777
this.invalidateFiles(ssrInvalidates)
7878
}
@@ -140,7 +140,7 @@ export class ViteRuntime {
140140
files.forEach((file) => {
141141
const ids = this.fileToIdMap.get(file)
142142
if (ids) {
143-
ids.forEach((id) => this.moduleCache.deleteByModuleId(id))
143+
ids.forEach((id) => this.moduleCache.invalidate(id))
144144
}
145145
})
146146
}

packages/vite/types/hmrPayload.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface PrunePayload {
3636
export interface FullReloadPayload {
3737
type: 'full-reload'
3838
path?: string
39+
/** @internal */
40+
triggeredBy?: string
3941
}
4042

4143
export interface CustomPayload {

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,55 @@ describe('acceptExports', () => {
506506
)
507507
})
508508
})
509+
510+
describe("doesn't reload if files not in the the entrypoint importers chain is changed", async () => {
511+
const testFile = 'non-tested/index.js'
512+
513+
beforeAll(async () => {
514+
clientLogs.length = 0
515+
// so it's in the module graph
516+
await server.transformRequest(testFile, { ssr: true })
517+
await server.transformRequest('non-tested/dep.js', { ssr: true })
518+
})
519+
520+
test('does not full reload', async () => {
521+
editFile(
522+
testFile,
523+
(code) => code + '\n\nexport const query5 = "query5"',
524+
)
525+
const start = Date.now()
526+
// for 2 seconds check that there is no log about the file being reloaded
527+
while (Date.now() - start < 2000) {
528+
if (
529+
clientLogs.some(
530+
(log) =>
531+
log.match(PROGRAM_RELOAD) ||
532+
log.includes('non-tested/index.js'),
533+
)
534+
) {
535+
throw new Error('File was reloaded')
536+
}
537+
await new Promise((r) => setTimeout(r, 100))
538+
}
539+
}, 5_000)
540+
541+
test('does not update', async () => {
542+
editFile('non-tested/dep.js', (code) => code + '//comment')
543+
const start = Date.now()
544+
// for 2 seconds check that there is no log about the file being reloaded
545+
while (Date.now() - start < 2000) {
546+
if (
547+
clientLogs.some(
548+
(log) =>
549+
log.match(PROGRAM_RELOAD) || log.includes('non-tested/dep.js'),
550+
)
551+
) {
552+
throw new Error('File was updated')
553+
}
554+
await new Promise((r) => setTimeout(r, 100))
555+
}
556+
}, 5_000)
557+
})
509558
})
510559

511560
test('accepts itself when imported for side effects only (no bindings imported)', async () => {

playground/hmr-ssr/non-tested/dep.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const test = 'true'
2+
3+
import.meta.hot.accept()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from './dep.js'
2+
3+
function main() {
4+
test()
5+
}
6+
7+
main()
8+
9+
import.meta.hot.accept('./dep.js')

0 commit comments

Comments
 (0)