1
+ import path from 'node:path'
1
2
import type { RolldownBuild , RolldownOptions } from 'rolldown'
2
3
import type { Update } from 'types/hmrPayload'
3
4
import colors from 'picocolors'
@@ -11,7 +12,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections'
11
12
import { DevEnvironment , type DevEnvironmentContext } from '../environment'
12
13
import type { ResolvedConfig } from '../../config'
13
14
import type { ViteDevServer } from '../../server'
14
- import { arraify , createDebugger } from '../../utils'
15
+ import { arraify , createDebugger , normalizePath } from '../../utils'
15
16
import { prepareError } from '../middlewares/error'
16
17
17
18
const debug = createDebugger ( 'vite:full-bundle-mode' )
@@ -57,7 +58,6 @@ export class FullBundleDevEnvironment extends DevEnvironment {
57
58
async onFileChange (
58
59
_type : 'create' | 'update' | 'delete' ,
59
60
file : string ,
60
- server : ViteDevServer ,
61
61
) : Promise < void > {
62
62
if ( this . state . type === 'initial' ) {
63
63
return
@@ -67,15 +67,21 @@ export class FullBundleDevEnvironment extends DevEnvironment {
67
67
debug ?.(
68
68
`BUNDLING: file update detected ${ file } , retriggering bundle generation` ,
69
69
)
70
- this . state . abortController . abort ( )
71
70
this . triggerGenerateBundle ( this . state )
72
71
return
73
72
}
74
73
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
+ }
79
85
return
80
86
}
81
87
@@ -95,35 +101,111 @@ export class FullBundleDevEnvironment extends DevEnvironment {
95
101
type : 'generating-hmr-patch' ,
96
102
options : this . state . options ,
97
103
bundle : this . state . bundle ,
104
+ patched : this . state . patched ,
98
105
}
99
106
100
107
let hmrOutput : HmrOutput
101
108
try {
102
109
// NOTE: only single outputOptions is supported here
103
- hmrOutput = ( await this . state . bundle . generateHmrPatch ( [ file ] ) ) !
110
+ hmrOutput = await this . state . bundle . generateHmrPatch ( [ file ] )
104
111
} catch ( e ) {
105
112
// 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 ] ) } )
107
114
108
115
this . state = {
109
116
type : 'bundled' ,
110
117
options : this . state . options ,
111
118
bundle : this . state . bundle ,
119
+ patched : this . state . patched ,
112
120
}
113
121
return
114
122
}
115
123
116
- debug ?.( `handle hmr output for ${ file } ` , {
117
- ...hmrOutput ,
118
- code : typeof hmrOutput . code === 'string' ? '[code]' : hmrOutput . code ,
119
- } )
120
-
121
124
this . handleHmrOutput ( file , hmrOutput , this . state )
122
125
return
123
126
}
124
127
this . state satisfies never // exhaustive check
125
128
}
126
129
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
+
127
209
override async close ( ) : Promise < void > {
128
210
await Promise . all ( [
129
211
super . close ( ) ,
@@ -161,6 +243,10 @@ export class FullBundleDevEnvironment extends DevEnvironment {
161
243
options,
162
244
bundle,
163
245
} : BundleStateCommonProperties ) {
246
+ if ( this . state . type === 'bundling' ) {
247
+ this . state . abortController . abort ( )
248
+ }
249
+
164
250
const controller = new AbortController ( )
165
251
const promise = this . generateBundle (
166
252
options . output ,
@@ -211,6 +297,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
211
297
type : 'bundled' ,
212
298
bundle : this . state . bundle ,
213
299
options : this . state . options ,
300
+ patched : false ,
214
301
}
215
302
debug ?.( 'BUNDLED: bundle generated' )
216
303
@@ -234,7 +321,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
234
321
}
235
322
}
236
323
237
- private async handleHmrOutput (
324
+ private handleHmrOutput (
238
325
file : string ,
239
326
hmrOutput : HmrOutput ,
240
327
{ options, bundle } : BundleStateCommonProperties ,
@@ -255,7 +342,16 @@ export class FullBundleDevEnvironment extends DevEnvironment {
255
342
return
256
343
}
257
344
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
+
259
355
this . memoryFiles . set ( hmrOutput . filename , hmrOutput . code )
260
356
if ( hmrOutput . sourcemapFilename && hmrOutput . sourcemap ) {
261
357
this . memoryFiles . set ( hmrOutput . sourcemapFilename , hmrOutput . sourcemap )
@@ -279,7 +375,17 @@ export class FullBundleDevEnvironment extends DevEnvironment {
279
375
colors . dim ( [ ...new Set ( updates . map ( ( u ) => u . path ) ) ] . join ( ', ' ) ) ,
280
376
{ clear : ! hmrOutput . firstInvalidatedBy , timestamp : true } ,
281
377
)
378
+
379
+ this . state = {
380
+ type : 'bundled' ,
381
+ options,
382
+ bundle,
383
+ patched : true ,
384
+ }
385
+ return
282
386
}
387
+
388
+ debug ?.( `ignored file change for ${ file } ` )
283
389
}
284
390
}
285
391
@@ -296,12 +402,26 @@ type BundleStateBundling = {
296
402
promise : Promise < void >
297
403
abortController : AbortController
298
404
} & 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
300
414
type BundleStateBundleError = {
301
415
type : 'bundle-error'
302
416
} & BundleStateCommonProperties
303
417
type BundleStateGeneratingHmrPatch = {
304
418
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
305
425
} & BundleStateCommonProperties
306
426
307
427
type BundleStateCommonProperties = {
0 commit comments