@@ -16,9 +16,14 @@ import { prepareError } from '../middlewares/error'
1616
1717const debug = createDebugger ( 'vite:full-bundle-mode' )
1818
19+ type HmrOutput = Exclude <
20+ Awaited < ReturnType < RolldownBuild [ 'generateHmrPatch' ] > > ,
21+ undefined
22+ >
23+
1924export class FullBundleDevEnvironment extends DevEnvironment {
20- private rolldownOptions : RolldownOptions | undefined
21- private bundle : RolldownBuild | undefined
25+ private state : BundleState = { type : 'initial' }
26+
2227 watchFiles = new Set < string > ( )
2328 memoryFiles = new Map < string , string | Uint8Array > ( )
2429
@@ -39,48 +44,211 @@ export class FullBundleDevEnvironment extends DevEnvironment {
3944 override async listen ( server : ViteDevServer ) : Promise < void > {
4045 await super . listen ( server )
4146
42- debug ?.( 'setup bundle options' )
47+ debug ?.( 'INITIAL: setup bundle options' )
4348 const rollupOptions = await this . getRolldownOptions ( )
4449 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' )
4852
49- this . triggerGenerateInitialBundle ( rollupOptions . output )
53+ debug ?.( 'BUNDLING: trigger initial bundle' )
54+ this . triggerGenerateBundle ( { options : rollupOptions , bundle } )
5055 }
5156
5257 async onFileChange (
5358 _type : 'create' | 'update' | 'delete' ,
5459 file : string ,
5560 server : ViteDevServer ,
5661 ) : 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+ }
5881
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+ }
6293
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
68101 try {
69- await this . generateBundle ( this . rolldownOptions ! . output )
102+ // NOTE: only single outputOptions is supported here
103+ hmrOutput = ( await this . state . bundle . generateHmrPatch ( [ file ] ) ) !
70104 } catch ( e ) {
71105 // TODO: support multiple errors
72106 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+ }
73113 return
74114 }
75115
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+
77245 const reason = hmrOutput . fullReloadReason
78246 ? colors . dim ( ` (${ hmrOutput . fullReloadReason } )` )
79247 : ''
80248 this . logger . info (
81- colors . green ( `page reload ` ) + colors . dim ( file ) + reason ,
249+ colors . green ( `trigger page reload ` ) + colors . dim ( file ) + reason ,
82250 {
83- clear : ! hmrOutput . firstInvalidatedBy ,
251+ // clear: !hmrOutput.firstInvalidatedBy,
84252 timestamp : true ,
85253 } ,
86254 )
@@ -102,7 +270,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
102270 timestamp : 0 ,
103271 }
104272 } )
105- server ! . ws . send ( {
273+ this . hot . send ( {
106274 type : 'update' ,
107275 updates,
108276 } )
@@ -113,62 +281,30 @@ export class FullBundleDevEnvironment extends DevEnvironment {
113281 )
114282 }
115283 }
284+ }
116285
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
168306
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
174310}
0 commit comments