@@ -15,9 +15,14 @@ import { prepareError } from '../middlewares/error'
1515
1616const debug = createDebugger ( 'vite:full-bundle-mode' )
1717
18+ type HmrOutput = Exclude <
19+ Awaited < ReturnType < RolldownBuild [ 'generateHmrPatch' ] > > ,
20+ undefined
21+ >
22+
1823export class FullBundleDevEnvironment extends DevEnvironment {
19- private rolldownOptions : RolldownOptions | undefined
20- private bundle : RolldownBuild | undefined
24+ private state : BundleState = { type : 'initial' }
25+
2126 watchFiles = new Set < string > ( )
2227 memoryFiles = new Map < string , string | Uint8Array > ( )
2328
@@ -38,48 +43,210 @@ export class FullBundleDevEnvironment extends DevEnvironment {
3843 override async listen ( server : ViteDevServer ) : Promise < void > {
3944 await super . listen ( server )
4045
41- debug ?.( 'setup bundle options' )
46+ debug ?.( 'INITIAL: setup bundle options' )
4247 const rollupOptions = await this . getRolldownOptions ( )
4348 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' )
4751
48- this . triggerGenerateInitialBundle ( rollupOptions . output )
52+ debug ?.( 'BUNDLING: trigger initial bundle' )
53+ this . triggerGenerateBundle ( { options : rollupOptions , bundle } )
4954 }
5055
5156 async onFileChange (
5257 _type : 'create' | 'update' | 'delete' ,
5358 file : string ,
5459 server : ViteDevServer ,
5560 ) : Promise < void > {
56- // TODO: handle the case when the initial bundle is not generated yet
61+ if ( this . state . type === 'initial' ) {
62+ return
63+ }
5764
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+ }
6180
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
67100 try {
68- await this . generateBundle ( this . rolldownOptions ! . output )
101+ // NOTE: only single outputOptions is supported here
102+ hmrOutput = ( await this . state . bundle . generateHmrPatch ( [ file ] ) ) !
69103 } catch ( e ) {
70104 // TODO: support multiple errors
71105 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+ }
72112 return
73113 }
74114
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+
76243 const reason = hmrOutput . fullReloadReason
77244 ? colors . dim ( ` (${ hmrOutput . fullReloadReason } )` )
78245 : ''
79246 this . logger . info (
80- colors . green ( `page reload ` ) + colors . dim ( file ) + reason ,
247+ colors . green ( `trigger page reload ` ) + colors . dim ( file ) + reason ,
81248 {
82- clear : ! hmrOutput . firstInvalidatedBy ,
249+ // clear: !hmrOutput.firstInvalidatedBy,
83250 timestamp : true ,
84251 } ,
85252 )
@@ -101,7 +268,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
101268 timestamp : 0 ,
102269 }
103270 } )
104- server ! . ws . send ( {
271+ this . hot . send ( {
105272 type : 'update' ,
106273 updates,
107274 } )
@@ -112,61 +279,30 @@ export class FullBundleDevEnvironment extends DevEnvironment {
112279 )
113280 }
114281 }
282+ }
115283
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
166304
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
172308}
0 commit comments