@@ -16,9 +16,14 @@ import { prepareError } from '../middlewares/error'
16
16
17
17
const debug = createDebugger ( 'vite:full-bundle-mode' )
18
18
19
+ type HmrOutput = Exclude <
20
+ Awaited < ReturnType < RolldownBuild [ 'generateHmrPatch' ] > > ,
21
+ undefined
22
+ >
23
+
19
24
export class FullBundleDevEnvironment extends DevEnvironment {
20
- private rolldownOptions : RolldownOptions | undefined
21
- private bundle : RolldownBuild | undefined
25
+ private state : BundleState = { type : 'initial' }
26
+
22
27
watchFiles = new Set < string > ( )
23
28
memoryFiles = new Map < string , string | Uint8Array > ( )
24
29
@@ -39,48 +44,211 @@ export class FullBundleDevEnvironment extends DevEnvironment {
39
44
override async listen ( server : ViteDevServer ) : Promise < void > {
40
45
await super . listen ( server )
41
46
42
- debug ?.( 'setup bundle options' )
47
+ debug ?.( 'INITIAL: setup bundle options' )
43
48
const rollupOptions = await this . getRolldownOptions ( )
44
49
const { rolldown } = await import ( 'rolldown' )
45
- this . rolldownOptions = rollupOptions
46
- this . bundle = await rolldown ( rollupOptions )
47
- debug ?.( 'bundle created' )
50
+ const bundle = await rolldown ( rollupOptions )
51
+ debug ?.( 'INITIAL: bundle created' )
48
52
49
- this . triggerGenerateInitialBundle ( rollupOptions . output )
53
+ debug ?.( 'BUNDLING: trigger initial bundle' )
54
+ this . triggerGenerateBundle ( { options : rollupOptions , bundle } )
50
55
}
51
56
52
57
async onFileChange (
53
58
_type : 'create' | 'update' | 'delete' ,
54
59
file : string ,
55
60
server : ViteDevServer ,
56
61
) : Promise < void > {
57
- // TODO: handle the case when the initial bundle is not generated yet
62
+ if ( this . state . type === 'initial' ) {
63
+ return
64
+ }
65
+
66
+ if ( this . state . type === 'bundling' ) {
67
+ debug ?.(
68
+ `BUNDLING: file update detected ${ file } , retriggering bundle generation` ,
69
+ )
70
+ this . state . abortController . abort ( )
71
+ this . triggerGenerateBundle ( this . state )
72
+ return
73
+ }
74
+ if ( this . state . type === 'bundle-error' ) {
75
+ debug ?.(
76
+ `BUNDLE-ERROR: file update detected ${ file } , retriggering bundle generation` ,
77
+ )
78
+ this . triggerGenerateBundle ( this . state )
79
+ return
80
+ }
58
81
59
- debug ?.( `file update detected ${ file } , generating hmr patch` )
60
- // NOTE: only single outputOptions is supported here
61
- const hmrOutput = ( await this . bundle ! . generateHmrPatch ( [ file ] ) ) !
82
+ if (
83
+ this . state . type === 'bundled' ||
84
+ this . state . type === 'generating-hmr-patch'
85
+ ) {
86
+ if ( this . state . type === 'bundled' ) {
87
+ debug ?.( `BUNDLED: file update detected ${ file } , generating HMR patch` )
88
+ } else if ( this . state . type === 'generating-hmr-patch' ) {
89
+ debug ?.(
90
+ `GENERATING-HMR-PATCH: file update detected ${ file } , regenerating HMR patch` ,
91
+ )
92
+ }
62
93
63
- debug ?.( `handle hmr output for ${ file } ` , {
64
- ...hmrOutput ,
65
- code : typeof hmrOutput . code === 'string' ? '[code]' : hmrOutput . code ,
66
- } )
67
- if ( hmrOutput . fullReload ) {
94
+ this . state = {
95
+ type : 'generating-hmr-patch' ,
96
+ options : this . state . options ,
97
+ bundle : this . state . bundle ,
98
+ }
99
+
100
+ let hmrOutput : HmrOutput
68
101
try {
69
- await this . generateBundle ( this . rolldownOptions ! . output )
102
+ // NOTE: only single outputOptions is supported here
103
+ hmrOutput = ( await this . state . bundle . generateHmrPatch ( [ file ] ) ) !
70
104
} catch ( e ) {
71
105
// TODO: support multiple errors
72
106
server . ws . send ( { type : 'error' , err : prepareError ( e . errors [ 0 ] ) } )
107
+
108
+ this . state = {
109
+ type : 'bundled' ,
110
+ options : this . state . options ,
111
+ bundle : this . state . bundle ,
112
+ }
73
113
return
74
114
}
75
115
76
- server . ws . send ( { type : 'full-reload' } )
116
+ debug ?.( `handle hmr output for ${ file } ` , {
117
+ ...hmrOutput ,
118
+ code : typeof hmrOutput . code === 'string' ? '[code]' : hmrOutput . code ,
119
+ } )
120
+
121
+ this . handleHmrOutput ( file , hmrOutput , this . state )
122
+ return
123
+ }
124
+ this . state satisfies never // exhaustive check
125
+ }
126
+
127
+ override async close ( ) : Promise < void > {
128
+ await Promise . all ( [
129
+ super . close ( ) ,
130
+ ( async ( ) => {
131
+ if ( this . state . type === 'initial' ) {
132
+ return
133
+ }
134
+ if ( this . state . type === 'bundling' ) {
135
+ this . state . abortController . abort ( )
136
+ }
137
+ const bundle = this . state . bundle
138
+ this . state = { type : 'initial' }
139
+
140
+ this . watchFiles . clear ( )
141
+ this . memoryFiles . clear ( )
142
+ await bundle . close ( )
143
+ } ) ( ) ,
144
+ ] )
145
+ }
146
+
147
+ private async getRolldownOptions ( ) {
148
+ const chunkMetadataMap = new Map < string , ChunkMetadata > ( )
149
+ const rolldownOptions = resolveRolldownOptions ( this , chunkMetadataMap )
150
+ rolldownOptions . experimental ??= { }
151
+ rolldownOptions . experimental . hmr = {
152
+ implement : await getHmrImplementation ( this . getTopLevelConfig ( ) ) ,
153
+ }
154
+
155
+ rolldownOptions . treeshake = false
156
+
157
+ return rolldownOptions
158
+ }
159
+
160
+ private triggerGenerateBundle ( {
161
+ options,
162
+ bundle,
163
+ } : BundleStateCommonProperties ) {
164
+ const controller = new AbortController ( )
165
+ const promise = this . generateBundle (
166
+ options . output ,
167
+ bundle ,
168
+ controller . signal ,
169
+ )
170
+ this . state = {
171
+ type : 'bundling' ,
172
+ options,
173
+ bundle,
174
+ promise,
175
+ abortController : controller ,
176
+ }
177
+ }
178
+
179
+ private async generateBundle (
180
+ outOpts : RolldownOptions [ 'output' ] ,
181
+ bundle : RolldownBuild ,
182
+ signal : AbortSignal ,
183
+ ) {
184
+ try {
185
+ const newMemoryFiles = new Map < string , string | Uint8Array > ( )
186
+ for ( const outputOpts of arraify ( outOpts ) ) {
187
+ const output = await bundle . generate ( outputOpts )
188
+ if ( signal . aborted ) return
189
+
190
+ for ( const outputFile of output . output ) {
191
+ newMemoryFiles . set (
192
+ outputFile . fileName ,
193
+ outputFile . type === 'chunk' ? outputFile . code : outputFile . source ,
194
+ )
195
+ }
196
+ }
197
+
198
+ this . memoryFiles . clear ( )
199
+ for ( const [ file , code ] of newMemoryFiles ) {
200
+ this . memoryFiles . set ( file , code )
201
+ }
202
+
203
+ // TODO: should this be done for hmr patch file generation?
204
+ for ( const file of await bundle . watchFiles ) {
205
+ this . watchFiles . add ( file )
206
+ }
207
+ if ( signal . aborted ) return
208
+
209
+ if ( this . state . type === 'initial' ) throw new Error ( 'unreachable' )
210
+ this . state = {
211
+ type : 'bundled' ,
212
+ bundle : this . state . bundle ,
213
+ options : this . state . options ,
214
+ }
215
+ debug ?.( 'BUNDLED: bundle generated' )
216
+
217
+ this . hot . send ( { type : 'full-reload' } )
218
+ this . logger . info ( colors . green ( `page reload` ) , { timestamp : true } )
219
+ } catch ( e ) {
220
+ enhanceRollupError ( e )
221
+ clearLine ( )
222
+ this . logger . error ( `${ colors . red ( '✗' ) } Build failed` + e . stack )
223
+
224
+ // TODO: support multiple errors
225
+ this . hot . send ( { type : 'error' , err : prepareError ( e . errors [ 0 ] ) } )
226
+
227
+ if ( this . state . type === 'initial' ) throw new Error ( 'unreachable' )
228
+ this . state = {
229
+ type : 'bundle-error' ,
230
+ bundle : this . state . bundle ,
231
+ options : this . state . options ,
232
+ }
233
+ debug ?.( 'BUNDLED: bundle errored' )
234
+ }
235
+ }
236
+
237
+ private async handleHmrOutput (
238
+ file : string ,
239
+ hmrOutput : HmrOutput ,
240
+ { options, bundle } : BundleStateCommonProperties ,
241
+ ) {
242
+ if ( hmrOutput . fullReload ) {
243
+ this . triggerGenerateBundle ( { options, bundle } )
244
+
77
245
const reason = hmrOutput . fullReloadReason
78
246
? colors . dim ( ` (${ hmrOutput . fullReloadReason } )` )
79
247
: ''
80
248
this . logger . info (
81
- colors . green ( `page reload ` ) + colors . dim ( file ) + reason ,
249
+ colors . green ( `trigger page reload ` ) + colors . dim ( file ) + reason ,
82
250
{
83
- clear : ! hmrOutput . firstInvalidatedBy ,
251
+ // clear: !hmrOutput.firstInvalidatedBy,
84
252
timestamp : true ,
85
253
} ,
86
254
)
@@ -102,7 +270,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
102
270
timestamp : 0 ,
103
271
}
104
272
} )
105
- server ! . ws . send ( {
273
+ this . hot . send ( {
106
274
type : 'update' ,
107
275
updates,
108
276
} )
@@ -113,62 +281,30 @@ export class FullBundleDevEnvironment extends DevEnvironment {
113
281
)
114
282
}
115
283
}
284
+ }
116
285
117
- override async close ( ) : Promise < void > {
118
- await Promise . all ( [
119
- super . close ( ) ,
120
- this . bundle ?. close ( ) . finally ( ( ) => {
121
- this . bundle = undefined
122
- this . watchFiles . clear ( )
123
- this . memoryFiles . clear ( )
124
- } ) ,
125
- ] )
126
- }
127
-
128
- private async getRolldownOptions ( ) {
129
- const chunkMetadataMap = new Map < string , ChunkMetadata > ( )
130
- const rolldownOptions = resolveRolldownOptions ( this , chunkMetadataMap )
131
- rolldownOptions . experimental ??= { }
132
- rolldownOptions . experimental . hmr = {
133
- implement : await getHmrImplementation ( this . getTopLevelConfig ( ) ) ,
134
- }
135
-
136
- rolldownOptions . treeshake = false
137
-
138
- return rolldownOptions
139
- }
140
-
141
- private async triggerGenerateInitialBundle (
142
- outOpts : RolldownOptions [ 'output' ] ,
143
- ) {
144
- this . generateBundle ( outOpts ) . then (
145
- ( ) => {
146
- debug ?.( 'initial bundle generated' )
147
- } ,
148
- ( e ) => {
149
- enhanceRollupError ( e )
150
- clearLine ( )
151
- this . logger . error ( `${ colors . red ( '✗' ) } Build failed` + e . stack )
152
- // TODO: show error message on the browser
153
- } ,
154
- )
155
- }
156
-
157
- // TODO: should debounce this
158
- private async generateBundle ( outOpts : RolldownOptions [ 'output' ] ) {
159
- for ( const outputOpts of arraify ( outOpts ) ) {
160
- const output = await this . bundle ! . generate ( outputOpts )
161
- for ( const outputFile of output . output ) {
162
- this . memoryFiles . set (
163
- outputFile . fileName ,
164
- outputFile . type === 'chunk' ? outputFile . code : outputFile . source ,
165
- )
166
- }
167
- }
286
+ // https://mermaid.live/edit#pako:eNqdUk1v4jAQ_SujuRSkFAUMJOSwalWuPXVPq0jIjYfEWmeMHKe0i_jvaxJoqcRuUX2x3se8mZFmh4VVhBmujd0WlXQefi5zhvAaH9Bg0H3DIdze_gDN2mtpev0IOuG5ZWU0l71yQkECcs66Dw-tOuLMd2QO3rU2BGEILumL1OudTVsU1DRnE6jz5upSWklMTvqQsKpqt9pIX1R90SXl0pbq__bTUIPADr9RxhY-V76v_q_S61bsM-7vdtBUckMZeHr1ERj5TCaDHLcVMRC_aGe5JvagGyiMbUhFoD1stTFQWvAWbo7XcZMj7HPGCGtytdQqnNru0CZHX1FNOR5ylXS_c8x5H3yy9fbpjQvMvGspQmfbssJsLU0TULtR0tNSy9LJ-p3dSP5lbX0qIaW9dY_9YXf3HWHpDr2PkcSK3INt2WM2XswnXQJmO3wNOJmOxCIdx0ksRDwX4zTCN8zS-SidTRbxNAlkIvYR_uk6xqNkFk9TMZ2JSSKSREz2fwERkhWq
287
+ type BundleState =
288
+ | BundleStateInitial
289
+ | BundleStateBundling
290
+ | BundleStateBundled
291
+ | BundleStateBundleError
292
+ | BundleStateGeneratingHmrPatch
293
+ type BundleStateInitial = { type : 'initial' }
294
+ type BundleStateBundling = {
295
+ type : 'bundling'
296
+ promise : Promise < void >
297
+ abortController : AbortController
298
+ } & BundleStateCommonProperties
299
+ type BundleStateBundled = { type : 'bundled' } & BundleStateCommonProperties
300
+ type BundleStateBundleError = {
301
+ type : 'bundle-error'
302
+ } & BundleStateCommonProperties
303
+ type BundleStateGeneratingHmrPatch = {
304
+ type : 'generating-hmr-patch'
305
+ } & BundleStateCommonProperties
168
306
169
- // TODO: should this be done for hmr patch file generation?
170
- for ( const file of await this . bundle ! . watchFiles ) {
171
- this . watchFiles . add ( file )
172
- }
173
- }
307
+ type BundleStateCommonProperties = {
308
+ options : RolldownOptions
309
+ bundle : RolldownBuild
174
310
}
0 commit comments