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