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