Skip to content

Commit f2c7d7a

Browse files
committed
wip: full bundle dev env
1 parent 7bdd8a9 commit f2c7d7a

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
@@ -236,7 +236,7 @@ export class DevEnvironment extends BaseEnvironment {
236236
}
237237
}
238238

239-
private invalidateModule(m: {
239+
protected invalidateModule(m: {
240240
path: string
241241
message?: string
242242
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'
@@ -11,7 +12,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections'
1112
import { DevEnvironment, type DevEnvironmentContext } from '../environment'
1213
import type { ResolvedConfig } from '../../config'
1314
import type { ViteDevServer } from '../../server'
14-
import { arraify, createDebugger } from '../../utils'
15+
import { arraify, createDebugger, normalizePath } from '../../utils'
1516
import { prepareError } from '../middlewares/error'
1617

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

@@ -95,35 +101,111 @@ export class FullBundleDevEnvironment extends DevEnvironment {
95101
type: 'generating-hmr-patch',
96102
options: this.state.options,
97103
bundle: this.state.bundle,
104+
patched: this.state.patched,
98105
}
99106

100107
let hmrOutput: HmrOutput
101108
try {
102109
// NOTE: only single outputOptions is supported here
103-
hmrOutput = (await this.state.bundle.generateHmrPatch([file]))!
110+
hmrOutput = await this.state.bundle.generateHmrPatch([file])
104111
} catch (e) {
105112
// TODO: support multiple errors
106-
server.ws.send({ type: 'error', err: prepareError(e.errors[0]) })
113+
this.hot.send({ type: 'error', err: prepareError(e.errors[0]) })
107114

108115
this.state = {
109116
type: 'bundled',
110117
options: this.state.options,
111118
bundle: this.state.bundle,
119+
patched: this.state.patched,
112120
}
113121
return
114122
}
115123

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

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

@@ -234,7 +321,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
234321
}
235322
}
236323

237-
private async handleHmrOutput(
324+
private handleHmrOutput(
238325
file: string,
239326
hmrOutput: HmrOutput,
240327
{ options, bundle }: BundleStateCommonProperties,
@@ -255,7 +342,16 @@ export class FullBundleDevEnvironment extends DevEnvironment {
255342
return
256343
}
257344

258-
if (hmrOutput.code) {
345+
// TODO: handle `No corresponding module found for changed file path`
346+
if (
347+
hmrOutput.code &&
348+
hmrOutput.code !== '__rolldown_runtime__.applyUpdates([]);'
349+
) {
350+
debug?.(`handle hmr output for ${file}`, {
351+
...hmrOutput,
352+
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
353+
})
354+
259355
this.memoryFiles.set(hmrOutput.filename, hmrOutput.code)
260356
if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) {
261357
this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap)
@@ -279,7 +375,17 @@ export class FullBundleDevEnvironment extends DevEnvironment {
279375
colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
280376
{ clear: !hmrOutput.firstInvalidatedBy, timestamp: true },
281377
)
378+
379+
this.state = {
380+
type: 'bundled',
381+
options,
382+
bundle,
383+
patched: true,
384+
}
385+
return
282386
}
387+
388+
debug?.(`ignored file change for ${file}`)
283389
}
284390
}
285391

@@ -296,12 +402,26 @@ type BundleStateBundling = {
296402
promise: Promise<void>
297403
abortController: AbortController
298404
} & BundleStateCommonProperties
299-
type BundleStateBundled = { type: 'bundled' } & BundleStateCommonProperties
405+
type BundleStateBundled = {
406+
type: 'bundled'
407+
/**
408+
* Whether a hmr patch was generated.
409+
*
410+
* In other words, whether the bundle is stale.
411+
*/
412+
patched: boolean
413+
} & BundleStateCommonProperties
300414
type BundleStateBundleError = {
301415
type: 'bundle-error'
302416
} & BundleStateCommonProperties
303417
type BundleStateGeneratingHmrPatch = {
304418
type: 'generating-hmr-patch'
419+
/**
420+
* Whether a hmr patch was generated.
421+
*
422+
* In other words, whether the bundle is stale.
423+
*/
424+
patched: boolean
305425
} & BundleStateCommonProperties
306426

307427
type BundleStateCommonProperties = {

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

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

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,9 +442,9 @@ export function indexHtmlMiddleware(
442442
server: ViteDevServer | PreviewServer,
443443
): Connect.NextHandleFunction {
444444
const isDev = isDevServer(server)
445-
const memoryFiles =
445+
const fullBundleEnv =
446446
isDev && server.environments.client instanceof FullBundleDevEnvironment
447-
? server.environments.client.memoryFiles
447+
? server.environments.client
448448
: undefined
449449

450450
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -456,13 +456,18 @@ export function indexHtmlMiddleware(
456456
const url = req.url && cleanUrl(req.url)
457457
// htmlFallbackMiddleware appends '.html' to URLs
458458
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
459-
if (memoryFiles) {
459+
if (fullBundleEnv) {
460460
const cleanedUrl = cleanUrl(url).slice(1) // remove first /
461-
let content = memoryFiles.get(cleanedUrl)
462-
if (!content && memoryFiles.size !== 0) {
461+
let content = fullBundleEnv.memoryFiles.get(cleanedUrl)
462+
if (!content && fullBundleEnv.memoryFiles.size !== 0) {
463463
return next()
464464
}
465-
content ??= await generateFallbackHtml(server as ViteDevServer)
465+
if (
466+
fullBundleEnv.triggerBundleRegenerationIfStale() ||
467+
content === undefined
468+
) {
469+
content = await generateFallbackHtml(server as ViteDevServer)
470+
}
466471

467472
const html =
468473
typeof content === 'string' ? content : Buffer.from(content.buffer)

0 commit comments

Comments
 (0)