1+ import path from 'node:path'
12import type { RolldownBuild , RolldownOptions } from 'rolldown'
23import type { Update } from 'types/hmrPayload'
34import colors from 'picocolors'
@@ -10,7 +11,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections'
1011import { DevEnvironment , type DevEnvironmentContext } from '../environment'
1112import type { ResolvedConfig } from '../../config'
1213import type { ViteDevServer } from '../../server'
13- import { arraify , createDebugger } from '../../utils'
14+ import { arraify , createDebugger , normalizePath } from '../../utils'
1415import { prepareError } from '../middlewares/error'
1516
1617const 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
298412type BundleStateBundleError = {
299413 type : 'bundle-error'
300414} & BundleStateCommonProperties
301415type 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
305425type BundleStateCommonProperties = {
0 commit comments