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'
@@ -10,7 +11,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections'
10
11
import { DevEnvironment , type DevEnvironmentContext } from '../environment'
11
12
import type { ResolvedConfig } from '../../config'
12
13
import type { ViteDevServer } from '../../server'
13
- import { arraify , createDebugger } from '../../utils'
14
+ import { arraify , createDebugger , normalizePath } from '../../utils'
14
15
import { prepareError } from '../middlewares/error'
15
16
16
17
const debug = createDebugger ( 'vite:full-bundle-mode' )
@@ -56,7 +57,6 @@ export class FullBundleDevEnvironment extends DevEnvironment {
56
57
async onFileChange (
57
58
_type : 'create' | 'update' | 'delete' ,
58
59
file : string ,
59
- server : ViteDevServer ,
60
60
) : Promise < void > {
61
61
if ( this . state . type === 'initial' ) {
62
62
return
@@ -66,15 +66,21 @@ export class FullBundleDevEnvironment extends DevEnvironment {
66
66
debug ?.(
67
67
`BUNDLING: file update detected ${ file } , retriggering bundle generation` ,
68
68
)
69
- this . state . abortController . abort ( )
70
69
this . triggerGenerateBundle ( this . state )
71
70
return
72
71
}
73
72
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
+ }
78
84
return
79
85
}
80
86
@@ -94,35 +100,111 @@ export class FullBundleDevEnvironment extends DevEnvironment {
94
100
type : 'generating-hmr-patch' ,
95
101
options : this . state . options ,
96
102
bundle : this . state . bundle ,
103
+ patched : this . state . patched ,
97
104
}
98
105
99
106
let hmrOutput : HmrOutput
100
107
try {
101
108
// NOTE: only single outputOptions is supported here
102
- hmrOutput = ( await this . state . bundle . generateHmrPatch ( [ file ] ) ) !
109
+ hmrOutput = await this . state . bundle . generateHmrPatch ( [ file ] )
103
110
} catch ( e ) {
104
111
// 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 ] ) } )
106
113
107
114
this . state = {
108
115
type : 'bundled' ,
109
116
options : this . state . options ,
110
117
bundle : this . state . bundle ,
118
+ patched : this . state . patched ,
111
119
}
112
120
return
113
121
}
114
122
115
- debug ?.( `handle hmr output for ${ file } ` , {
116
- ...hmrOutput ,
117
- code : typeof hmrOutput . code === 'string' ? '[code]' : hmrOutput . code ,
118
- } )
119
-
120
123
this . handleHmrOutput ( file , hmrOutput , this . state )
121
124
return
122
125
}
123
126
this . state satisfies never // exhaustive check
124
127
}
125
128
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
+
126
208
override async close ( ) : Promise < void > {
127
209
await Promise . all ( [
128
210
super . close ( ) ,
@@ -159,6 +241,10 @@ export class FullBundleDevEnvironment extends DevEnvironment {
159
241
options,
160
242
bundle,
161
243
} : BundleStateCommonProperties ) {
244
+ if ( this . state . type === 'bundling' ) {
245
+ this . state . abortController . abort ( )
246
+ }
247
+
162
248
const controller = new AbortController ( )
163
249
const promise = this . generateBundle (
164
250
options . output ,
@@ -209,6 +295,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
209
295
type : 'bundled' ,
210
296
bundle : this . state . bundle ,
211
297
options : this . state . options ,
298
+ patched : false ,
212
299
}
213
300
debug ?.( 'BUNDLED: bundle generated' )
214
301
@@ -232,7 +319,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
232
319
}
233
320
}
234
321
235
- private async handleHmrOutput (
322
+ private handleHmrOutput (
236
323
file : string ,
237
324
hmrOutput : HmrOutput ,
238
325
{ options, bundle } : BundleStateCommonProperties ,
@@ -253,7 +340,16 @@ export class FullBundleDevEnvironment extends DevEnvironment {
253
340
return
254
341
}
255
342
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
+
257
353
this . memoryFiles . set ( hmrOutput . filename , hmrOutput . code )
258
354
if ( hmrOutput . sourcemapFilename && hmrOutput . sourcemap ) {
259
355
this . memoryFiles . set ( hmrOutput . sourcemapFilename , hmrOutput . sourcemap )
@@ -277,7 +373,17 @@ export class FullBundleDevEnvironment extends DevEnvironment {
277
373
colors . dim ( [ ...new Set ( updates . map ( ( u ) => u . path ) ) ] . join ( ', ' ) ) ,
278
374
{ clear : ! hmrOutput . firstInvalidatedBy , timestamp : true } ,
279
375
)
376
+
377
+ this . state = {
378
+ type : 'bundled' ,
379
+ options,
380
+ bundle,
381
+ patched : true ,
382
+ }
383
+ return
280
384
}
385
+
386
+ debug ?.( `ignored file change for ${ file } ` )
281
387
}
282
388
}
283
389
@@ -294,12 +400,26 @@ type BundleStateBundling = {
294
400
promise : Promise < void >
295
401
abortController : AbortController
296
402
} & 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
298
412
type BundleStateBundleError = {
299
413
type : 'bundle-error'
300
414
} & BundleStateCommonProperties
301
415
type BundleStateGeneratingHmrPatch = {
302
416
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
303
423
} & BundleStateCommonProperties
304
424
305
425
type BundleStateCommonProperties = {
0 commit comments