Skip to content

Commit 4f0949f

Browse files
authored
feat(bundled-dev): add lazy bundling support (#21406)
1 parent 158e8ae commit 4f0949f

9 files changed

Lines changed: 105 additions & 14 deletions

File tree

packages/vite/src/client/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,15 @@ if (isBundleMode && typeof DevRuntime !== 'undefined') {
644644
}
645645
}
646646

647+
const clientId = nanoid()
648+
649+
// notify client id
650+
transport.send({
651+
type: 'custom',
652+
event: 'vite:module-loaded',
653+
data: { modules: [], clientId },
654+
})
655+
647656
const wrappedSocket: Messenger = {
648657
send(message) {
649658
switch (message.type) {
@@ -652,7 +661,7 @@ if (isBundleMode && typeof DevRuntime !== 'undefined') {
652661
type: 'custom',
653662
event: 'vite:module-loaded',
654663
// clone array as the runtime reuses the array instance
655-
data: { modules: message.modules.slice() },
664+
data: { modules: message.modules.slice(), clientId },
656665
})
657666
break
658667
}
@@ -661,7 +670,6 @@ if (isBundleMode && typeof DevRuntime !== 'undefined') {
661670
}
662671
},
663672
}
664-
const clientId = nanoid()
665673
;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime(
666674
wrappedSocket,
667675
clientId,

packages/vite/src/node/server/environments/fullBundleEnvironment.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { randomUUID } from 'node:crypto'
21
import { setTimeout } from 'node:timers/promises'
32
import {
43
type BindingClientHmrUpdate,
@@ -61,6 +60,7 @@ export class MemoryFiles {
6160

6261
export class FullBundleDevEnvironment extends DevEnvironment {
6362
private devEngine!: DevEngine
63+
private initialBuildCompleted = false
6464
private clients = new Clients()
6565
private invalidateCalledModules = new Map<
6666
NormalizedHotChannelClient,
@@ -106,8 +106,8 @@ export class FullBundleDevEnvironment extends DevEnvironment {
106106
)!
107107

108108
this.hot.on('vite:module-loaded', (payload, client) => {
109-
const clientId = this.clients.setupIfNeeded(client)
110-
this.devEngine.registerModules(clientId, payload.modules)
109+
this.clients.setupIfNeeded(client, payload.clientId)
110+
this.devEngine.registerModules(payload.clientId, payload.modules)
111111
})
112112
this.hot.on('vite:client:disconnect', (_payload, client) => {
113113
const clientId = this.clients.delete(client)
@@ -184,6 +184,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
184184
this.waitForInitialBuildFinish().then(() => {
185185
debug?.('INITIAL: build done')
186186
this.hot.send({ type: 'full-reload', path: '*' })
187+
this.initialBuildCompleted = true
187188
})
188189
}
189190

@@ -260,7 +261,9 @@ export class FullBundleDevEnvironment extends DevEnvironment {
260261
async triggerBundleRegenerationIfStale(): Promise<boolean> {
261262
const bundleState = await this.devEngine.getBundleState()
262263
const shouldTrigger =
263-
bundleState.hasStaleOutput && !bundleState.lastFullBuildFailed
264+
bundleState.hasStaleOutput &&
265+
!bundleState.lastFullBuildFailed &&
266+
this.initialBuildCompleted
264267
if (shouldTrigger) {
265268
this.devEngine.ensureLatestBuildOutput().then(() => {
266269
this.debouncedFullReload()
@@ -270,16 +273,34 @@ export class FullBundleDevEnvironment extends DevEnvironment {
270273
return shouldTrigger
271274
}
272275

276+
async triggerLazyBundling(
277+
moduleId: string | null,
278+
clientId: string | null,
279+
): Promise<string | undefined> {
280+
if (!moduleId || !clientId) {
281+
return
282+
}
283+
debug?.(
284+
`TRIGGER-LAZY: trigger lazy bundling for module ${moduleId} for client ${clientId}`,
285+
)
286+
return await this.devEngine.compileEntry(moduleId, clientId)
287+
}
288+
273289
override async close(): Promise<void> {
274290
this.memoryFiles.clear()
275291
await Promise.all([super.close(), this.devEngine.close()])
292+
this.initialBuildCompleted = false
276293
}
277294

278295
private async getRolldownOptions() {
279296
const chunkMetadataMap = new ChunkMetadataMap()
280297
const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap)
281298
rolldownOptions.experimental ??= {}
282299
rolldownOptions.experimental.devMode = {
300+
lazy: true,
301+
...(typeof rolldownOptions.experimental.devMode === 'object'
302+
? rolldownOptions.experimental.devMode
303+
: {}),
283304
implement: await getHmrImplementation(this.getTopLevelConfig()),
284305
}
285306

@@ -382,14 +403,15 @@ class Clients {
382403
private clientToId = new Map<NormalizedHotChannelClient, string>()
383404
private idToClient = new Map<string, NormalizedHotChannelClient>()
384405

385-
setupIfNeeded(client: NormalizedHotChannelClient): string {
406+
setupIfNeeded(client: NormalizedHotChannelClient, clientId: string) {
386407
const id = this.clientToId.get(client)
387-
if (id) return id
388-
389-
const newId = randomUUID()
390-
this.clientToId.set(client, newId)
391-
this.idToClient.set(newId, client)
392-
return newId
408+
if (id && id !== clientId) {
409+
throw new Error(
410+
'client ID conflict detected. Please restart the dev server.',
411+
)
412+
}
413+
this.clientToId.set(client, clientId)
414+
this.idToClient.set(clientId, client)
393415
}
394416

395417
get(id: string): NormalizedHotChannelClient | undefined {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import type { DevEnvironment } from './environment'
107107
import { hostValidationMiddleware } from './middlewares/hostCheck'
108108
import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest'
109109
import { memoryFilesMiddleware } from './middlewares/memoryFiles'
110+
import { triggerLazyBundlingMiddleware } from './middlewares/triggerLazyBundling'
110111

111112
const usedConfigs = new WeakSet<ResolvedConfig>()
112113

@@ -993,6 +994,7 @@ export async function _createServer(
993994
}
994995

995996
if (config.experimental.bundledDev) {
997+
middlewares.use(triggerLazyBundlingMiddleware(server))
996998
middlewares.use(memoryFilesMiddleware(server))
997999
} else {
9981000
// main transform middleware
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Connect } from '#dep-types/connect'
2+
import type { ViteDevServer } from '..'
3+
import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment'
4+
5+
export function triggerLazyBundlingMiddleware(
6+
server: ViteDevServer,
7+
): Connect.NextHandleFunction {
8+
const environment =
9+
server.environments.client instanceof FullBundleDevEnvironment
10+
? server.environments.client
11+
: undefined
12+
if (!environment) {
13+
throw new Error(
14+
'triggerLazyBundlingMiddleware can only be used for fullBundleMode',
15+
)
16+
}
17+
18+
return async function viteTriggerLazyBundlingMiddleware(req, res, next) {
19+
if (!req.url?.startsWith('/@vite/lazy?')) {
20+
return next()
21+
}
22+
23+
let params: URLSearchParams
24+
try {
25+
params = new URL(`http://localhost${req.url}`).searchParams
26+
} catch {
27+
// Malformed URL
28+
return next()
29+
}
30+
31+
const moduleId = params.get('id')
32+
const clientId = params.get('clientId')
33+
const code = await environment.triggerLazyBundling(moduleId, clientId)
34+
if (code == null) {
35+
return next()
36+
}
37+
38+
res!.setHeader('Content-Type', 'application/javascript')
39+
return res!.end(code)
40+
}
41+
}

packages/vite/types/customEvent.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface CustomEventMap {
1818
/** @internal */
1919
'vite:forward-console': ForwardConsolePayload
2020
/** @internal */
21-
'vite:module-loaded': { modules: string[] }
21+
'vite:module-loaded': { modules: string[]; clientId: string }
2222

2323
// server events
2424
'vite:client:connect': undefined

playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,9 @@ if (isBuild) {
183183
)
184184
await expect.poll(() => page.textContent('.worker-url')).toBe('worker-url')
185185
})
186+
187+
test('lazy bundling', async () => {
188+
await page.click('#load-dynamic')
189+
await expect.poll(() => page.textContent('.dynamic')).toBe('loaded')
190+
})
186191
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
text('.dynamic', 'loaded')
2+
3+
function text(el, text) {
4+
document.querySelector(el).textContent = text
5+
}

playground/hmr-full-bundle-mode/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ <h1>HMR Full Bundle Mode</h1>
55
<div class="asset"></div>
66
<div class="worker-query"></div>
77
<div class="worker-url"></div>
8+
<div>
9+
<button id="load-dynamic">Load dynamic</button>
10+
<div class="dynamic"></div>
11+
</div>
812

913
<script type="module" src="./main.js"></script>

playground/hmr-full-bundle-mode/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ workerUrl.addEventListener('message', (e) => {
1919
text('.worker-url', e.data)
2020
})
2121

22+
document.querySelector('#load-dynamic').addEventListener('click', () => {
23+
import('./dynamic.js')
24+
})
25+
2226
function text(el, text) {
2327
document.querySelector(el).textContent = text
2428
}

0 commit comments

Comments
 (0)