Skip to content

Commit e85becb

Browse files
committed
wip: full bundle dev env
1 parent e4d61f5 commit e85becb

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'
@@ -596,67 +597,26 @@ export function injectQuery(url: string, queryToInject: string): string {
596597
export { ErrorOverlay }
597598

598599
if (isFullBundleMode) {
599-
class DevRuntime {
600-
modules: Record<string, { exports: any }> = {}
601-
602-
static getInstance() {
603-
// @ts-expect-error __rolldown_runtime__
604-
let instance = globalThis.__rolldown_runtime__
605-
if (!instance) {
606-
instance = new DevRuntime()
607-
// @ts-expect-error __rolldown_runtime__
608-
globalThis.__rolldown_runtime__ = instance
609-
}
610-
return instance
611-
}
612-
613-
createModuleHotContext(moduleId: string) {
600+
class ViteDevRuntime extends DevRuntime {
601+
override createModuleHotContext(moduleId: string) {
614602
const ctx = createHotContext(moduleId)
615603
// @ts-expect-error TODO: support CSS
616604
ctx._internal = {
617605
updateStyle,
618606
removeStyle,
619607
}
608+
// @ts-expect-error TODO: support this function (used by plugin-react)
609+
ctx.getExports = async () =>
610+
// @ts-expect-error __rolldown_runtime__ / ctx.ownerPath
611+
__rolldown_runtime__.loadExports(ctx.ownerPath)
620612
return ctx
621613
}
622614

623-
applyUpdates(_boundaries: string[]) {
624-
//
625-
}
626-
627-
registerModule(
628-
id: string,
629-
module: { exports: Record<string, () => unknown> },
630-
) {
631-
this.modules[id] = module
615+
override applyUpdates(_boundaries: string[]): void {
616+
// TODO: how should this be handled?
617+
// noop, handled in the HMR client
632618
}
633-
634-
loadExports(id: string) {
635-
const module = this.modules[id]
636-
if (module) {
637-
return module.exports
638-
} else {
639-
console.warn(`Module ${id} not found`)
640-
return {}
641-
}
642-
}
643-
644-
// __esmMin
645-
// @ts-expect-error need to add typing
646-
createEsmInitializer = (fn, res) => () => (fn && (res = fn((fn = 0))), res)
647-
// __commonJSMin
648-
// @ts-expect-error need to add typing
649-
createCjsInitializer = (cb, mod) => () => (
650-
mod || cb((mod = { exports: {} }).exports, mod), mod.exports
651-
)
652-
// @ts-expect-error it is exits
653-
__toESM = __toESM
654-
// @ts-expect-error it is exits
655-
__toCommonJS = __toCommonJS
656-
// @ts-expect-error it is exits
657-
__export = __export
658619
}
659620

660-
// @ts-expect-error __rolldown_runtime__
661-
globalThis.__rolldown_runtime__ ||= new DevRuntime()
621+
;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime()
662622
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export class DevEnvironment extends BaseEnvironment {
243243
}
244244
}
245245

246-
private invalidateModule(m: {
246+
protected invalidateModule(m: {
247247
path: string
248248
message?: string
249249
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
@@ -422,7 +422,7 @@ export async function handleHMRUpdate(
422422
if (config.experimental.fullBundleMode) {
423423
// TODO: support handleHotUpdate / hotUpdate
424424
const environment = server.environments.client as FullBundleDevEnvironment
425-
environment.onFileChange(type, file, server)
425+
environment.onFileChange(type, file)
426426
return
427427
}
428428

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)