@@ -63,6 +63,7 @@ export function rolldownDevHandleConfig(
63
63
createEnvironment : RolldownEnvironment . createFactory ( {
64
64
hmr : config . experimental ?. rolldownDev ?. hmr ,
65
65
reactRefresh : config . experimental ?. rolldownDev ?. reactRefresh ,
66
+ ssrModuleRunner : false ,
66
67
} ) ,
67
68
} ,
68
69
build : {
@@ -81,6 +82,7 @@ export function rolldownDevHandleConfig(
81
82
createEnvironment : RolldownEnvironment . createFactory ( {
82
83
hmr : false ,
83
84
reactRefresh : false ,
85
+ ssrModuleRunner : config . experimental ?. rolldownDev ?. ssrModuleRunner ,
84
86
} ) ,
85
87
} ,
86
88
} ,
@@ -134,6 +136,8 @@ class RolldownEnvironment extends DevEnvironment {
134
136
result ! : rolldown . RolldownOutput
135
137
outDir ! : string
136
138
buildTimestamp = Date . now ( )
139
+ inputOptions ! : rolldown . InputOptions
140
+ outputOptions ! : rolldown . OutputOptions
137
141
138
142
static createFactory (
139
143
rolldownDevOptioins : RolldownDevOptions ,
@@ -200,7 +204,7 @@ class RolldownEnvironment extends DevEnvironment {
200
204
plugins = plugins . map ( ( p ) => injectEnvironmentToHooks ( this as any , p ) )
201
205
202
206
console . time ( `[rolldown:${ this . name } :build]` )
203
- const inputOptions : rolldown . InputOptions = {
207
+ this . inputOptions = {
204
208
dev : this . rolldownDevOptions . hmr ,
205
209
input : this . config . build . rollupOptions . input ,
206
210
cwd : this . config . root ,
@@ -212,30 +216,34 @@ class RolldownEnvironment extends DevEnvironment {
212
216
} ,
213
217
plugins : [
214
218
...plugins ,
215
- patchRuntimePlugin ( this . rolldownDevOptions ) ,
219
+ patchRuntimePlugin ( this ) ,
216
220
patchCssPlugin ( ) ,
217
221
reactRefreshPlugin ( ) ,
218
222
] ,
219
223
moduleTypes : {
220
224
'.css' : 'js' ,
221
225
} ,
222
226
}
223
- this . instance = await rolldown . rolldown ( inputOptions )
227
+ this . instance = await rolldown . rolldown ( this . inputOptions )
224
228
225
- // `generate` should work but we use `write` so it's easier to see output and debug
226
- const outputOptions : rolldown . OutputOptions = {
229
+ const format : rolldown . ModuleFormat =
230
+ this . name === 'client' || this . rolldownDevOptions . ssrModuleRunner
231
+ ? 'app'
232
+ : 'esm'
233
+ this . outputOptions = {
227
234
dir : this . outDir ,
228
- format : this . rolldownDevOptions . hmr ? 'app' : 'esm' ,
235
+ format,
229
236
// TODO: hmr_rebuild returns source map file when `sourcemap: true`
230
237
sourcemap : 'inline' ,
231
238
// TODO: https://github.com/rolldown/rolldown/issues/2041
232
239
// handle `require("stream")` in `react-dom/server`
233
240
banner :
234
- this . name === 'ssr'
241
+ this . name === 'ssr' && format === 'esm'
235
242
? `import __nodeModule from "node:module"; const require = __nodeModule.createRequire(import.meta.url);`
236
243
: undefined ,
237
244
}
238
- this . result = await this . instance . write ( outputOptions )
245
+ // `generate` should work but we use `write` so it's easier to see output and debug
246
+ this . result = await this . instance . write ( this . outputOptions )
239
247
240
248
this . buildTimestamp = Date . now ( )
241
249
console . timeEnd ( `[rolldown:${ this . name } :build]` )
@@ -249,12 +257,19 @@ class RolldownEnvironment extends DevEnvironment {
249
257
if ( ! output . moduleIds . includes ( ctx . file ) ) {
250
258
return
251
259
}
252
- if ( this . rolldownDevOptions . hmr ) {
260
+ if (
261
+ this . rolldownDevOptions . hmr ||
262
+ this . rolldownDevOptions . ssrModuleRunner
263
+ ) {
253
264
logger . info ( `hmr '${ ctx . file } '` , { timestamp : true } )
254
265
console . time ( `[rolldown:${ this . name } :hmr]` )
255
266
const result = await this . instance . experimental_hmr_rebuild ( [ ctx . file ] )
267
+ if ( this . name === 'client' ) {
268
+ ctx . server . ws . send ( 'rolldown:hmr' , result )
269
+ } else {
270
+ this . getRunner ( ) . evaluate ( result [ 1 ] . toString ( ) )
271
+ }
256
272
console . timeEnd ( `[rolldown:${ this . name } :hmr]` )
257
- ctx . server . ws . send ( 'rolldown:hmr' , result )
258
273
} else {
259
274
await this . build ( )
260
275
if ( this . name === 'client' ) {
@@ -263,40 +278,136 @@ class RolldownEnvironment extends DevEnvironment {
263
278
}
264
279
}
265
280
281
+ runner ! : RolldownModuleRunner
282
+
283
+ getRunner ( ) {
284
+ if ( ! this . runner ) {
285
+ const output = this . result . output [ 0 ]
286
+ const filepath = path . join ( this . outDir , output . fileName )
287
+ this . runner = new RolldownModuleRunner ( )
288
+ const code = fs . readFileSync ( filepath , 'utf-8' )
289
+ this . runner . evaluate ( code )
290
+ }
291
+ return this . runner
292
+ }
293
+
266
294
async import ( input : string ) : Promise < unknown > {
267
- const output = this . result . output . find ( ( o ) => o . name === input )
268
- assert ( output , `invalid import input '${ input } '` )
295
+ if ( this . outputOptions . format === 'app' ) {
296
+ return this . getRunner ( ) . import ( input )
297
+ }
298
+ // input is no use
299
+ const output = this . result . output [ 0 ]
269
300
const filepath = path . join ( this . outDir , output . fileName )
270
301
return import ( `${ pathToFileURL ( filepath ) } ?t=${ this . buildTimestamp } ` )
271
302
}
272
303
}
273
304
274
- function patchRuntimePlugin (
275
- rolldownDevOptions : RolldownDevOptions ,
276
- ) : rolldown . Plugin {
305
+ class RolldownModuleRunner {
306
+ // intercept globals
307
+ private context = {
308
+ rolldown_runtime : { } as any ,
309
+ __rolldown_hot : {
310
+ send : ( ) => { } ,
311
+ } ,
312
+ // TODO: external require doesn't work in app format.
313
+ // TODO: also it should be aware of importer for non static require/import.
314
+ _require : require ,
315
+ }
316
+
317
+ // TODO: support resolution?
318
+ async import ( id : string ) : Promise < unknown > {
319
+ const mod = this . context . rolldown_runtime . moduleCache [ id ]
320
+ assert ( mod , `Module not found '${ id } '` )
321
+ return mod . exports
322
+ }
323
+
324
+ evaluate ( code : string ) {
325
+ const context = {
326
+ self : this . context ,
327
+ ...this . context ,
328
+ }
329
+ // TODO: sourcemap not working?
330
+ // extract sourcemap
331
+ const sourcemap = code . match ( / ^ \/ \/ # s o u r c e M a p p i n g U R L = .* / m) ?. [ 0 ] ?? ''
332
+ if ( sourcemap ) {
333
+ code = code . replace ( sourcemap , '' )
334
+ }
335
+ code = `\
336
+ 'use strict';(${ Object . keys ( context ) . join ( ',' ) } )=>{{${ code }
337
+ // TODO: need to re-expose runtime utilities for now
338
+ self.__toCommonJS = __toCommonJS;
339
+ self.__export = __export;
340
+ self.__toESM = __toESM;
341
+ }}
342
+ //# sourceMappingSource=rolldown-module-runner
343
+ ${ sourcemap }
344
+ `
345
+ const fn = ( 0 , eval ) ( code )
346
+ try {
347
+ fn ( ...Object . values ( context ) )
348
+ } catch ( e ) {
349
+ console . error ( '[RolldownModuleRunner:ERROR]' , e )
350
+ throw e
351
+ }
352
+ }
353
+ }
354
+
355
+ function patchRuntimePlugin ( environment : RolldownEnvironment ) : rolldown . Plugin {
277
356
return {
278
357
name : 'vite:rolldown-patch-runtime' ,
358
+ // TODO: external require doesn't work in app format.
359
+ // rewrite `require -> _require` and provide _require from module runner.
360
+ // for now just rewrite known ones in "react-dom/server".
361
+ transform : {
362
+ filter : {
363
+ code : {
364
+ include : [ / r e q u i r e \( [ ' " ] ( s t r e a m | u t i l ) [ ' " ] \) / ] ,
365
+ } ,
366
+ } ,
367
+ handler ( code ) {
368
+ if ( ! environment . rolldownDevOptions . ssrModuleRunner ) {
369
+ return
370
+ }
371
+ return code . replace (
372
+ / r e q u i r e ( \( [ ' " ] ( s t r e a m | u t i l ) [ ' " ] \) ) / g,
373
+ '_require($1)' ,
374
+ )
375
+ } ,
376
+ } ,
279
377
renderChunk ( code ) {
280
378
// patch rolldown_runtime to workaround a few things
281
379
// TODO: is there a robust way to inject code specifically to entry or runtime?
282
380
if ( code . includes ( '//#region rolldown:runtime' ) ) {
283
- // TODO: is this magic string heavy?
381
+ // TODO: this magic string is heavy
284
382
const output = new MagicString ( code )
285
- // replace hard-coded WebSocket setup with custom client
286
- output . replace ( / c o n s t s o c k e t = .* ?\n \} ; / s, getRolldownClientCode ( ) )
287
- // trigger full rebuild on non-accepting entry invalidation
288
383
output
384
+ // replace hard-coded WebSocket setup with custom client
385
+ . replace (
386
+ / c o n s t s o c k e t = .* ?\n \} ; / s,
387
+ environment . name === 'client' ? getRolldownClientCode ( ) : '' ,
388
+ )
389
+ // fix rolldown_runtime.patch
390
+ . replace (
391
+ 'this.executeModuleStack.length > 1' ,
392
+ 'this.executeModuleStack.length > 0' ,
393
+ )
289
394
. replace ( 'parents: [parent],' , 'parents: parent ? [parent] : [],' )
395
+ . replace (
396
+ 'if (module.parents.indexOf(parent) === -1) {' ,
397
+ 'if (parent && module.parents.indexOf(parent) === -1) {' ,
398
+ )
290
399
. replace (
291
400
'for (var i = 0; i < module.parents.length; i++) {' ,
292
401
`
293
- if (module.parents.length === 0) {
402
+ boundaries.push(moduleId);
403
+ invalidModuleIds.push(moduleId);
404
+ if (module.parents.filter(Boolean).length === 0) {
294
405
__rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
295
406
break;
296
407
}
297
408
for (var i = 0; i < module.parents.length; i++) {` ,
298
409
)
299
- if ( rolldownDevOptions . reactRefresh ) {
410
+ if ( environment . rolldownDevOptions . reactRefresh ) {
300
411
output . prepend ( getReactRefreshRuntimeCode ( ) )
301
412
}
302
413
return {
0 commit comments