1
+ import { randomUUID } from 'node:crypto'
1
2
import type { RolldownBuild } from 'rolldown'
2
3
import { type DevEngine , dev } from 'rolldown/experimental'
3
4
import type { Update } from 'types/hmrPayload'
@@ -9,6 +10,7 @@ import type { ResolvedConfig } from '../../config'
9
10
import type { ViteDevServer } from '../../server'
10
11
import { createDebugger } from '../../utils'
11
12
import { getShortName } from '../hmr'
13
+ import type { WebSocketClient } from '../ws'
12
14
13
15
const debug = createDebugger ( 'vite:full-bundle-mode' )
14
16
@@ -58,7 +60,11 @@ export class MemoryFiles {
58
60
59
61
export class FullBundleDevEnvironment extends DevEnvironment {
60
62
private devEngine ! : DevEngine
61
- private invalidateCalledModules = new Set < string > ( )
63
+ private clients = new Clients ( )
64
+ private invalidateCalledModules = new Map <
65
+ /* clientId */ string ,
66
+ Set < string >
67
+ > ( )
62
68
private debouncedFullReload = debounce ( 20 , ( ) => {
63
69
this . hot . send ( { type : 'full-reload' , path : '*' } )
64
70
this . logger . info ( colors . green ( `page reload` ) , { timestamp : true } )
@@ -80,7 +86,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
80
86
super ( name , config , { ...context , disableDepsOptimizer : true } )
81
87
}
82
88
83
- override async listen ( _server : ViteDevServer ) : Promise < void > {
89
+ override async listen ( server : ViteDevServer ) : Promise < void > {
84
90
this . hot . listen ( )
85
91
86
92
debug ?.( 'INITIAL: setup bundle options' )
@@ -98,19 +104,39 @@ export class FullBundleDevEnvironment extends DevEnvironment {
98
104
: rollupOptions . output
99
105
) !
100
106
107
+ // TODO: use hot API
108
+ server . ws . on (
109
+ 'vite:module-loaded' ,
110
+ ( payload : { modules : string [ ] } , client : WebSocketClient ) => {
111
+ const clientId = this . clients . setupIfNeeded ( client , ( ) => {
112
+ this . devEngine . removeClient ( clientId )
113
+ } )
114
+ this . devEngine . registerModules ( clientId , payload . modules )
115
+ } ,
116
+ )
117
+ server . ws . on ( 'vite:invalidate' , ( payload , client : WebSocketClient ) => {
118
+ this . handleInvalidateModule ( client , payload )
119
+ } )
120
+
101
121
this . devEngine = await dev ( rollupOptions , outputOptions , {
102
122
onHmrUpdates : ( updates , files ) => {
103
123
if ( files . length === 0 ) {
104
124
return
105
125
}
106
- // TODO: how to handle errors?
107
- if ( updates . every ( ( update ) => update . type === 'Noop' ) ) {
126
+ // TODO: fix the need to clone
127
+ const clonedUpdates = updates . map ( ( u ) => ( {
128
+ clientId : u . clientId ,
129
+ update : { ...u . update } ,
130
+ } ) )
131
+ if ( clonedUpdates . every ( ( update ) => update . update . type === 'Noop' ) ) {
108
132
debug ?.( `ignored file change for ${ files . join ( ', ' ) } ` )
109
133
return
110
134
}
111
- this . invalidateCalledModules . clear ( )
112
- for ( const update of updates ) {
113
- this . handleHmrOutput ( files , update )
135
+ // TODO: how to handle errors?
136
+ for ( const { clientId, update } of clonedUpdates ) {
137
+ this . invalidateCalledModules . get ( clientId ) ?. clear ( )
138
+ const client = this . clients . get ( clientId ) !
139
+ this . handleHmrOutput ( client , files , update )
114
140
}
115
141
} ,
116
142
watch : {
@@ -144,13 +170,24 @@ export class FullBundleDevEnvironment extends DevEnvironment {
144
170
// no-op
145
171
}
146
172
147
- protected override invalidateModule ( m : {
148
- path : string
149
- message ?: string
150
- firstInvalidatedBy : string
151
- } ) : void {
173
+ protected override invalidateModule ( _m : unknown ) : void {
174
+ // no-op, handled via `server.ws` instead
175
+ }
176
+
177
+ private handleInvalidateModule (
178
+ client : WebSocketClient ,
179
+ m : {
180
+ path : string
181
+ message ?: string
182
+ firstInvalidatedBy : string
183
+ } ,
184
+ ) : void {
152
185
; ( async ( ) => {
153
- if ( this . invalidateCalledModules . has ( m . path ) ) {
186
+ const clientId = this . clients . getId ( client )
187
+ if ( ! clientId ) return
188
+
189
+ const invalidateCalledModules = this . invalidateCalledModules . get ( clientId )
190
+ if ( invalidateCalledModules ?. has ( m . path ) ) {
154
191
debug ?.(
155
192
`INVALIDATE: invalidate received from ${ m . path } , but ignored because it was already invalidated` ,
156
193
)
@@ -160,13 +197,18 @@ export class FullBundleDevEnvironment extends DevEnvironment {
160
197
debug ?.(
161
198
`INVALIDATE: invalidate received from ${ m . path } , re-triggering HMR` ,
162
199
)
163
- this . invalidateCalledModules . add ( m . path )
200
+ if ( ! invalidateCalledModules ) {
201
+ this . invalidateCalledModules . set ( clientId , new Set ( [ ] ) )
202
+ }
203
+ this . invalidateCalledModules . get ( clientId ) ! . add ( m . path )
164
204
165
205
// TODO: how to handle errors?
166
- const update = await this . devEngine . invalidate (
206
+ const _update = await this . devEngine . invalidate (
167
207
m . path ,
168
208
m . firstInvalidatedBy ,
169
209
)
210
+ const update = _update . find ( ( u ) => u . clientId === clientId ) ?. update
211
+ if ( ! update ) return
170
212
171
213
if ( update . type === 'Patch' ) {
172
214
this . logger . info (
@@ -178,7 +220,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
178
220
}
179
221
180
222
// TODO: need to check if this is enough
181
- this . handleHmrOutput ( [ m . path ] , update , {
223
+ this . handleHmrOutput ( client , [ m . path ] , update , {
182
224
firstInvalidatedBy : m . firstInvalidatedBy ,
183
225
} )
184
226
} ) ( )
@@ -265,6 +307,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
265
307
}
266
308
267
309
private handleHmrOutput (
310
+ client : WebSocketClient ,
268
311
files : string [ ] ,
269
312
hmrOutput : HmrOutput ,
270
313
invalidateInformation ?: { firstInvalidatedBy : string } ,
@@ -307,7 +350,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
307
350
timestamp : Date . now ( ) ,
308
351
}
309
352
} )
310
- this . hot . send ( {
353
+ client . send ( {
311
354
type : 'update' ,
312
355
updates,
313
356
} )
@@ -319,6 +362,42 @@ export class FullBundleDevEnvironment extends DevEnvironment {
319
362
}
320
363
}
321
364
365
+ class Clients {
366
+ private clientToId = new Map < WebSocketClient , string > ( )
367
+ private idToClient = new Map < string , WebSocketClient > ( )
368
+
369
+ setupIfNeeded ( client : WebSocketClient , onClose ?: ( ) => void ) : string {
370
+ const id = this . clientToId . get ( client )
371
+ if ( id ) return id
372
+
373
+ const newId = randomUUID ( )
374
+ this . clientToId . set ( client , newId )
375
+ this . idToClient . set ( newId , client )
376
+ client . socket . once ( 'close' , ( ) => {
377
+ this . clientToId . delete ( client )
378
+ this . idToClient . delete ( newId )
379
+ onClose ?.( )
380
+ } )
381
+ return newId
382
+ }
383
+
384
+ get ( id : string ) : WebSocketClient | undefined {
385
+ return this . idToClient . get ( id )
386
+ }
387
+
388
+ getId ( client : WebSocketClient ) : string | undefined {
389
+ return this . clientToId . get ( client )
390
+ }
391
+
392
+ delete ( client : WebSocketClient ) : void {
393
+ const id = this . clientToId . get ( client )
394
+ if ( id ) {
395
+ this . clientToId . delete ( client )
396
+ this . idToClient . delete ( id )
397
+ }
398
+ }
399
+ }
400
+
322
401
function debounce ( time : number , cb : ( ) => void ) {
323
402
let timer : ReturnType < typeof setTimeout > | null
324
403
return ( ) => {
0 commit comments