Skip to content

Commit 8100320

Browse files
authored
fix(bundled-dev): avoid duplicated buildEnd (#22931)
1 parent c88c236 commit 8100320

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class MemoryFiles {
6464
export class BundledDev {
6565
private _devEngine!: DevEngine
6666
private initialBuildCompleted = false
67+
private _closed = false
6768
private clients = new Clients()
6869
private invalidateCalledModules = new Map<
6970
NormalizedHotChannelClient,
@@ -98,6 +99,7 @@ export class BundledDev {
9899
}
99100

100101
async listen(): Promise<void> {
102+
this._closed = false
101103
debug?.('INITIAL: setup bundle options')
102104
const rolldownOptions = await this.getRolldownOptions()
103105
// NOTE: only single outputOptions is supported here
@@ -206,18 +208,24 @@ export class BundledDev {
206208
},
207209
)
208210
this.waitForInitialBuildFinish().then(() => {
211+
if (this._closed) return
209212
debug?.('INITIAL: build done')
210213
this.environment.hot.send({ type: 'full-reload', path: '*' })
211214
this.initialBuildCompleted = true
212215
})
213216
}
214217

215218
private async waitForInitialBuildFinish(): Promise<void> {
219+
if (this._closed) return
216220
await this.devEngine.ensureCurrentBuildFinish()
221+
if (this._closed) return
222+
217223
let state = await this.devEngine.getBundleState()
218224
while (this.memoryFiles.size === 0 && !state.lastBuildErrored) {
219225
await setTimeout(10)
226+
if (this._closed) return
220227
await this.devEngine.ensureCurrentBuildFinish()
228+
if (this._closed) return
221229
state = await this.devEngine.getBundleState()
222230
}
223231
}
@@ -322,6 +330,7 @@ export class BundledDev {
322330
}
323331

324332
async close(): Promise<void> {
333+
this._closed = true
325334
this.memoryFiles.clear()
326335
await this._devEngine?.close()
327336
this.initialBuildCompleted = false

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,7 @@ export class DevEnvironment extends BaseEnvironment {
329329

330330
this._crawlEndFinder.cancel()
331331
await Promise.allSettled([
332-
this.pluginContainer.close(),
333-
this.bundledDev?.close(),
332+
this.bundledDev ? this.bundledDev.close() : this.pluginContainer.close(),
334333
this.depsOptimizer?.close(),
335334
// WebSocketServer is independent of HotChannel and should not be closed on environment close
336335
isWebSocketServer in this.hot ? Promise.resolve() : this.hot.close(),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import path from 'node:path'
2+
import { type Plugin, type ViteDevServer, createServer } from 'vite'
3+
import { afterEach, describe, expect, test } from 'vitest'
4+
import { isServe } from '~utils'
5+
6+
let server: ViteDevServer | undefined
7+
8+
afterEach(async () => {
9+
await server?.close()
10+
server = undefined
11+
})
12+
13+
// In full bundle mode the build is driven by Rolldown's dev engine, which is the
14+
// one that invokes plugins' `buildStart`/`buildEnd` hooks. Regression test for
15+
// `buildEnd` being called twice because both `bundledDev.close()` and
16+
// `pluginContainer.close()` fired it on server close.
17+
describe.runIf(isServe)('full bundle mode build hooks', () => {
18+
test('buildStart and buildEnd are each called only once', async () => {
19+
let buildStartCount = 0
20+
let buildEndCount = 0
21+
const countPlugin: Plugin = {
22+
name: 'count-build-hooks',
23+
buildStart() {
24+
buildStartCount++
25+
},
26+
buildEnd() {
27+
buildEndCount++
28+
},
29+
}
30+
31+
server = await createServer({
32+
root: path.resolve(import.meta.dirname, '..'),
33+
configFile: false,
34+
logLevel: 'silent',
35+
experimental: { bundledDev: true },
36+
plugins: [countPlugin],
37+
})
38+
await server.listen()
39+
40+
// the initial full-bundle build runs on listen and fires buildStart + buildEnd once
41+
await expect.poll(() => buildEndCount, { timeout: 10000 }).toBe(1)
42+
expect(buildStartCount).toBe(1)
43+
44+
await server.close()
45+
server = undefined
46+
47+
// closing must not fire buildStart/buildEnd again
48+
expect(buildStartCount).toBe(1)
49+
expect(buildEndCount).toBe(1)
50+
})
51+
})

0 commit comments

Comments
 (0)