Skip to content

Commit 8db9ad7

Browse files
committed
wip: full bundle dev env
1 parent 9e22453 commit 8db9ad7

File tree

5 files changed

+161
-76
lines changed

5 files changed

+161
-76
lines changed

packages/vite/src/client/client.ts

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="rolldown/experimental/runtime-types" />
12
import type { ErrorPayload, HotPayload } from 'types/hmrPayload'
23
import type { ViteHotContext } from 'types/hot'
34
import { HMRClient, HMRContext } from '../shared/hmr'
@@ -483,67 +484,26 @@ export function injectQuery(url: string, queryToInject: string): string {
483484
export { ErrorOverlay }
484485

485486
if (isFullBundleMode) {
486-
class DevRuntime {
487-
modules: Record<string, { exports: any }> = {}
488-
489-
static getInstance() {
490-
// @ts-expect-error __rolldown_runtime__
491-
let instance = globalThis.__rolldown_runtime__
492-
if (!instance) {
493-
instance = new DevRuntime()
494-
// @ts-expect-error __rolldown_runtime__
495-
globalThis.__rolldown_runtime__ = instance
496-
}
497-
return instance
498-
}
499-
500-
createModuleHotContext(moduleId: string) {
487+
class ViteDevRuntime extends DevRuntime {
488+
override createModuleHotContext(moduleId: string) {
501489
const ctx = createHotContext(moduleId)
502490
// @ts-expect-error TODO: support CSS
503491
ctx._internal = {
504492
updateStyle,
505493
removeStyle,
506494
}
495+
// @ts-expect-error TODO: support this function (used by plugin-react)
496+
ctx.getExports = async () =>
497+
// @ts-expect-error __rolldown_runtime__ / ctx.ownerPath
498+
__rolldown_runtime__.loadExports(ctx.ownerPath)
507499
return ctx
508500
}
509501

510-
applyUpdates(_boundaries: string[]) {
511-
//
512-
}
513-
514-
registerModule(
515-
id: string,
516-
module: { exports: Record<string, () => unknown> },
517-
) {
518-
this.modules[id] = module
502+
override applyUpdates(_boundaries: string[]): void {
503+
// TODO: how should this be handled?
504+
// noop, handled in the HMR client
519505
}
520-
521-
loadExports(id: string) {
522-
const module = this.modules[id]
523-
if (module) {
524-
return module.exports
525-
} else {
526-
console.warn(`Module ${id} not found`)
527-
return {}
528-
}
529-
}
530-
531-
// __esmMin
532-
// @ts-expect-error need to add typing
533-
createEsmInitializer = (fn, res) => () => (fn && (res = fn((fn = 0))), res)
534-
// __commonJSMin
535-
// @ts-expect-error need to add typing
536-
createCjsInitializer = (cb, mod) => () => (
537-
mod || cb((mod = { exports: {} }).exports, mod), mod.exports
538-
)
539-
// @ts-expect-error it is exits
540-
__toESM = __toESM
541-
// @ts-expect-error it is exits
542-
__toCommonJS = __toCommonJS
543-
// @ts-expect-error it is exits
544-
__export = __export
545506
}
546507

547-
// @ts-expect-error __rolldown_runtime__
548-
globalThis.__rolldown_runtime__ ||= new DevRuntime()
508+
;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime()
549509
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export class DevEnvironment extends BaseEnvironment {
239239
}
240240
}
241241

242-
private invalidateModule(m: {
242+
protected invalidateModule(m: {
243243
path: string
244244
message?: string
245245
firstInvalidatedBy: string

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

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'node:path'
12
import type { RolldownBuild, RolldownOptions } from 'rolldown'
23
import type { Update } from 'types/hmrPayload'
34
import colors from 'picocolors'
@@ -10,7 +11,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections'
1011
import { DevEnvironment, type DevEnvironmentContext } from '../environment'
1112
import type { ResolvedConfig } from '../../config'
1213
import type { ViteDevServer } from '../../server'
13-
import { arraify, createDebugger } from '../../utils'
14+
import { arraify, createDebugger, normalizePath } from '../../utils'
1415
import { prepareError } from '../middlewares/error'
1516

1617
const debug = createDebugger('vite:full-bundle-mode')
@@ -56,7 +57,6 @@ export class FullBundleDevEnvironment extends DevEnvironment {
5657
async onFileChange(
5758
_type: 'create' | 'update' | 'delete',
5859
file: string,
59-
server: ViteDevServer,
6060
): Promise<void> {
6161
if (this.state.type === 'initial') {
6262
return
@@ -66,15 +66,21 @@ export class FullBundleDevEnvironment extends DevEnvironment {
6666
debug?.(
6767
`BUNDLING: file update detected ${file}, retriggering bundle generation`,
6868
)
69-
this.state.abortController.abort()
7069
this.triggerGenerateBundle(this.state)
7170
return
7271
}
7372
if (this.state.type === 'bundle-error') {
74-
debug?.(
75-
`BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`,
76-
)
77-
this.triggerGenerateBundle(this.state)
73+
const files = await this.state.bundle.watchFiles
74+
if (files.includes(file)) {
75+
debug?.(
76+
`BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`,
77+
)
78+
this.triggerGenerateBundle(this.state)
79+
} else {
80+
debug?.(
81+
`BUNDLE-ERROR: file update detected ${file}, but ignored as it is not a dependency`,
82+
)
83+
}
7884
return
7985
}
8086

@@ -94,35 +100,111 @@ export class FullBundleDevEnvironment extends DevEnvironment {
94100
type: 'generating-hmr-patch',
95101
options: this.state.options,
96102
bundle: this.state.bundle,
103+
patched: this.state.patched,
97104
}
98105

99106
let hmrOutput: HmrOutput
100107
try {
101108
// NOTE: only single outputOptions is supported here
102-
hmrOutput = (await this.state.bundle.generateHmrPatch([file]))!
109+
hmrOutput = await this.state.bundle.generateHmrPatch([file])
103110
} catch (e) {
104111
// TODO: support multiple errors
105-
server.ws.send({ type: 'error', err: prepareError(e.errors[0]) })
112+
this.hot.send({ type: 'error', err: prepareError(e.errors[0]) })
106113

107114
this.state = {
108115
type: 'bundled',
109116
options: this.state.options,
110117
bundle: this.state.bundle,
118+
patched: this.state.patched,
111119
}
112120
return
113121
}
114122

115-
debug?.(`handle hmr output for ${file}`, {
116-
...hmrOutput,
117-
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
118-
})
119-
120123
this.handleHmrOutput(file, hmrOutput, this.state)
121124
return
122125
}
123126
this.state satisfies never // exhaustive check
124127
}
125128

129+
protected override invalidateModule(m: {
130+
path: string
131+
message?: string
132+
firstInvalidatedBy: string
133+
}): void {
134+
;(async () => {
135+
if (
136+
this.state.type === 'initial' ||
137+
this.state.type === 'bundling' ||
138+
this.state.type === 'bundle-error'
139+
) {
140+
debug?.(
141+
`${this.state.type.toUpperCase()}: invalidate received, but ignored`,
142+
)
143+
return
144+
}
145+
this.state.type satisfies 'bundled' | 'generating-hmr-patch' // exhaustive check
146+
147+
debug?.(
148+
`${this.state.type.toUpperCase()}: invalidate received, re-triggering HMR`,
149+
)
150+
151+
// TODO: should this be a separate state?
152+
this.state = {
153+
type: 'generating-hmr-patch',
154+
options: this.state.options,
155+
bundle: this.state.bundle,
156+
patched: this.state.patched,
157+
}
158+
159+
let hmrOutput: HmrOutput
160+
try {
161+
// NOTE: only single outputOptions is supported here
162+
hmrOutput = await this.state.bundle.hmrInvalidate(
163+
normalizePath(path.join(this.config.root, m.path)),
164+
m.firstInvalidatedBy,
165+
)
166+
} catch (e) {
167+
// TODO: support multiple errors
168+
this.hot.send({ type: 'error', err: prepareError(e.errors[0]) })
169+
170+
this.state = {
171+
type: 'bundled',
172+
options: this.state.options,
173+
bundle: this.state.bundle,
174+
patched: this.state.patched,
175+
}
176+
return
177+
}
178+
179+
if (hmrOutput.isSelfAccepting) {
180+
this.logger.info(
181+
colors.yellow(`hmr invalidate `) +
182+
colors.dim(m.path) +
183+
(m.message ? ` ${m.message}` : ''),
184+
{ timestamp: true },
185+
)
186+
}
187+
188+
// TODO: need to check if this is enough
189+
this.handleHmrOutput(m.path, hmrOutput, this.state)
190+
})()
191+
}
192+
193+
triggerBundleRegenerationIfStale(): boolean {
194+
if (
195+
(this.state.type === 'bundled' ||
196+
this.state.type === 'generating-hmr-patch') &&
197+
this.state.patched
198+
) {
199+
this.triggerGenerateBundle(this.state)
200+
debug?.(
201+
`${this.state.type.toUpperCase()}: access to stale bundle, triggered bundle re-generation`,
202+
)
203+
return true
204+
}
205+
return false
206+
}
207+
126208
override async close(): Promise<void> {
127209
await Promise.all([
128210
super.close(),
@@ -159,6 +241,10 @@ export class FullBundleDevEnvironment extends DevEnvironment {
159241
options,
160242
bundle,
161243
}: BundleStateCommonProperties) {
244+
if (this.state.type === 'bundling') {
245+
this.state.abortController.abort()
246+
}
247+
162248
const controller = new AbortController()
163249
const promise = this.generateBundle(
164250
options.output,
@@ -209,6 +295,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
209295
type: 'bundled',
210296
bundle: this.state.bundle,
211297
options: this.state.options,
298+
patched: false,
212299
}
213300
debug?.('BUNDLED: bundle generated')
214301

@@ -232,7 +319,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
232319
}
233320
}
234321

235-
private async handleHmrOutput(
322+
private handleHmrOutput(
236323
file: string,
237324
hmrOutput: HmrOutput,
238325
{ options, bundle }: BundleStateCommonProperties,
@@ -253,7 +340,16 @@ export class FullBundleDevEnvironment extends DevEnvironment {
253340
return
254341
}
255342

256-
if (hmrOutput.code) {
343+
// TODO: handle `No corresponding module found for changed file path`
344+
if (
345+
hmrOutput.code &&
346+
hmrOutput.code !== '__rolldown_runtime__.applyUpdates([]);'
347+
) {
348+
debug?.(`handle hmr output for ${file}`, {
349+
...hmrOutput,
350+
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
351+
})
352+
257353
this.memoryFiles.set(hmrOutput.filename, hmrOutput.code)
258354
if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) {
259355
this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap)
@@ -277,7 +373,17 @@ export class FullBundleDevEnvironment extends DevEnvironment {
277373
colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
278374
{ clear: !hmrOutput.firstInvalidatedBy, timestamp: true },
279375
)
376+
377+
this.state = {
378+
type: 'bundled',
379+
options,
380+
bundle,
381+
patched: true,
382+
}
383+
return
280384
}
385+
386+
debug?.(`ignored file change for ${file}`)
281387
}
282388
}
283389

@@ -294,12 +400,26 @@ type BundleStateBundling = {
294400
promise: Promise<void>
295401
abortController: AbortController
296402
} & BundleStateCommonProperties
297-
type BundleStateBundled = { type: 'bundled' } & BundleStateCommonProperties
403+
type BundleStateBundled = {
404+
type: 'bundled'
405+
/**
406+
* Whether a hmr patch was generated.
407+
*
408+
* In other words, whether the bundle is stale.
409+
*/
410+
patched: boolean
411+
} & BundleStateCommonProperties
298412
type BundleStateBundleError = {
299413
type: 'bundle-error'
300414
} & BundleStateCommonProperties
301415
type BundleStateGeneratingHmrPatch = {
302416
type: 'generating-hmr-patch'
417+
/**
418+
* Whether a hmr patch was generated.
419+
*
420+
* In other words, whether the bundle is stale.
421+
*/
422+
patched: boolean
303423
} & BundleStateCommonProperties
304424

305425
type BundleStateCommonProperties = {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ export async function handleHMRUpdate(
428428
if (config.experimental.fullBundleMode) {
429429
// TODO: support handleHotUpdate / hotUpdate
430430
const environment = server.environments.client as FullBundleDevEnvironment
431-
environment.onFileChange(type, file, server)
431+
environment.onFileChange(type, file)
432432
return
433433
}
434434

packages/vite/src/node/server/middlewares/indexHtml.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,9 @@ export function indexHtmlMiddleware(
435435
server: ViteDevServer | PreviewServer,
436436
): Connect.NextHandleFunction {
437437
const isDev = isDevServer(server)
438-
const memoryFiles =
438+
const fullBundleEnv =
439439
isDev && server.environments.client instanceof FullBundleDevEnvironment
440-
? server.environments.client.memoryFiles
440+
? server.environments.client
441441
: undefined
442442

443443
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -449,13 +449,18 @@ export function indexHtmlMiddleware(
449449
const url = req.url && cleanUrl(req.url)
450450
// htmlFallbackMiddleware appends '.html' to URLs
451451
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
452-
if (memoryFiles) {
452+
if (fullBundleEnv) {
453453
const cleanedUrl = cleanUrl(url).slice(1) // remove first /
454-
let content = memoryFiles.get(cleanedUrl)
455-
if (!content && memoryFiles.size !== 0) {
454+
let content = fullBundleEnv.memoryFiles.get(cleanedUrl)
455+
if (!content && fullBundleEnv.memoryFiles.size !== 0) {
456456
return next()
457457
}
458-
content ??= await generateFallbackHtml(server as ViteDevServer)
458+
if (
459+
fullBundleEnv.triggerBundleRegenerationIfStale() ||
460+
content === undefined
461+
) {
462+
content = await generateFallbackHtml(server as ViteDevServer)
463+
}
459464

460465
const html =
461466
typeof content === 'string' ? content : Buffer.from(content.buffer)

0 commit comments

Comments
 (0)