@@ -23,6 +23,7 @@ const require = createRequire(import.meta.url);
23
23
24
24
interface ViterollOptions {
25
25
reactRefresh ?: boolean ;
26
+ ssrModuleRunner ?: boolean ;
26
27
}
27
28
28
29
const logger = createLogger ( "info" , {
@@ -151,7 +152,9 @@ window.__rolldown_hot = hot;
151
152
export class RolldownEnvironment extends DevEnvironment {
152
153
instance ! : rolldown . RolldownBuild ;
153
154
result ! : rolldown . RolldownOutput ;
154
- outDir ! : string ;
155
+ outDir : string ;
156
+ inputOptions ! : rolldown . InputOptions ;
157
+ outputOptions ! : rolldown . OutputOptions ;
155
158
buildTimestamp = Date . now ( ) ;
156
159
157
160
static createFactory (
@@ -206,7 +209,7 @@ export class RolldownEnvironment extends DevEnvironment {
206
209
}
207
210
208
211
console . time ( `[rolldown:${ this . name } :build]` ) ;
209
- const inputOptions : rolldown . InputOptions = {
212
+ this . inputOptions = {
210
213
// TODO: no dev ssr for now
211
214
dev : this . name === "client" ,
212
215
// NOTE:
@@ -223,7 +226,7 @@ export class RolldownEnvironment extends DevEnvironment {
223
226
} ,
224
227
define : this . config . define ,
225
228
plugins : [
226
- viterollEntryPlugin ( this . config , this . viterollOptions ) ,
229
+ viterollEntryPlugin ( this . config , this . viterollOptions , this ) ,
227
230
// TODO: how to use jsx-dev-runtime?
228
231
rolldownExperimental . transformPlugin ( {
229
232
reactRefresh :
@@ -238,22 +241,27 @@ export class RolldownEnvironment extends DevEnvironment {
238
241
...( plugins as any ) ,
239
242
] ,
240
243
} ;
241
- this . instance = await rolldown . rolldown ( inputOptions ) ;
242
-
243
- // `generate` should work but we use `write` so it's easier to see output and debug
244
- const outputOptions : rolldown . OutputOptions = {
244
+ this . instance = await rolldown . rolldown ( this . inputOptions ) ;
245
+
246
+ const format : rolldown . ModuleFormat =
247
+ this . name === "client" ||
248
+ ( this . name === "ssr" && this . viterollOptions . ssrModuleRunner )
249
+ ? "app"
250
+ : "esm" ;
251
+ this . outputOptions = {
245
252
dir : this . outDir ,
246
- format : this . name === "client" ? "app" : "esm" ,
253
+ format,
247
254
// TODO: hmr_rebuild returns source map file when `sourcemap: true`
248
255
sourcemap : "inline" ,
249
256
// TODO: https://github.com/rolldown/rolldown/issues/2041
250
257
// handle `require("stream")` in `react-dom/server`
251
258
banner :
252
- this . name === "ssr"
259
+ this . name === "ssr" && format === "esm"
253
260
? `import __nodeModule from "node:module"; const require = __nodeModule.createRequire(import.meta.url);`
254
261
: undefined ,
255
262
} ;
256
- this . result = await this . instance . write ( outputOptions ) ;
263
+ // `generate` should work but we use `write` so it's easier to see output and debug
264
+ this . result = await this . instance . write ( this . outputOptions ) ;
257
265
258
266
this . buildTimestamp = Date . now ( ) ;
259
267
console . timeEnd ( `[rolldown:${ this . name } :build]` ) ;
@@ -268,29 +276,104 @@ export class RolldownEnvironment extends DevEnvironment {
268
276
return ;
269
277
}
270
278
if ( this . name === "ssr" ) {
271
- await this . build ( ) ;
279
+ if ( this . outputOptions . format === "app" ) {
280
+ console . time ( `[rolldown:${ this . name } :hmr]` ) ;
281
+ const result = await this . instance . experimental_hmr_rebuild ( [ ctx . file ] ) ;
282
+ this . getRunner ( ) . evaluate ( result [ 1 ] . toString ( ) , result [ 0 ] ) ;
283
+ console . timeEnd ( `[rolldown:${ this . name } :hmr]` ) ;
284
+ } else {
285
+ await this . build ( ) ;
286
+ }
272
287
} else {
273
288
logger . info ( `hmr '${ ctx . file } '` , { timestamp : true } ) ;
274
289
console . time ( `[rolldown:${ this . name } :hmr]` ) ;
275
290
const result = await this . instance . experimental_hmr_rebuild ( [ ctx . file ] ) ;
276
291
console . timeEnd ( `[rolldown:${ this . name } :hmr]` ) ;
277
292
ctx . server . ws . send ( "rolldown:hmr" , result ) ;
278
293
}
279
- return true ;
294
+ }
295
+
296
+ runner ! : RolldownModuleRunner ;
297
+
298
+ getRunner ( ) {
299
+ if ( ! this . runner ) {
300
+ const output = this . result . output [ 0 ] ;
301
+ const filepath = path . join ( this . outDir , output . fileName ) ;
302
+ this . runner = new RolldownModuleRunner ( ) ;
303
+ const code = fs . readFileSync ( filepath , "utf-8" ) ;
304
+ this . runner . evaluate ( code , filepath ) ;
305
+ }
306
+ return this . runner ;
280
307
}
281
308
282
309
async import ( input : string ) : Promise < unknown > {
283
- const output = this . result . output . find ( ( o ) => o . name === input ) ;
284
- assert ( output , `invalid import input '${ input } '` ) ;
310
+ if ( this . outputOptions . format === "app" ) {
311
+ return this . getRunner ( ) . import ( input ) ;
312
+ }
313
+ // input is no use
314
+ const output = this . result . output [ 0 ] ;
285
315
const filepath = path . join ( this . outDir , output . fileName ) ;
316
+ // TODO: source map not applied when adding `?t=...`?
317
+ // return import(`${pathToFileURL(filepath)}`)
286
318
return import ( `${ pathToFileURL ( filepath ) } ?t=${ this . buildTimestamp } ` ) ;
287
319
}
288
320
}
289
321
322
+ class RolldownModuleRunner {
323
+ // intercept globals
324
+ private context = {
325
+ rolldown_runtime : { } as any ,
326
+ __rolldown_hot : {
327
+ send : ( ) => { } ,
328
+ } ,
329
+ // TODO
330
+ // should be aware of importer for non static require/import.
331
+ // they needs to be transformed beforehand, so runtime can intercept.
332
+ require,
333
+ } ;
334
+
335
+ // TODO: support resolution?
336
+ async import ( id : string ) : Promise < unknown > {
337
+ const mod = this . context . rolldown_runtime . moduleCache [ id ] ;
338
+ assert ( mod , `Module not found '${ id } '` ) ;
339
+ return mod . exports ;
340
+ }
341
+
342
+ evaluate ( code : string , sourceURL : string ) {
343
+ const context = {
344
+ self : this . context ,
345
+ ...this . context ,
346
+ } ;
347
+ // extract sourcemap
348
+ const sourcemap = code . match ( / ^ \/ \/ # s o u r c e M a p p i n g U R L = .* / m) ?. [ 0 ] ?? "" ;
349
+ if ( sourcemap ) {
350
+ code = code . replace ( sourcemap , "" ) ;
351
+ }
352
+ // as eval
353
+ code = `\
354
+ 'use strict';(${ Object . keys ( context ) . join ( "," ) } )=>{{${ code }
355
+ // TODO: need to re-expose runtime utilities for now
356
+ self.__toCommonJS = __toCommonJS;
357
+ self.__export = __export;
358
+ self.__toESM = __toESM;
359
+ }}
360
+ //# sourceURL=${ sourceURL }
361
+ ${ sourcemap }
362
+ ` ;
363
+ try {
364
+ const fn = ( 0 , eval ) ( code ) ;
365
+ fn ( ...Object . values ( context ) ) ;
366
+ } catch ( e ) {
367
+ console . error ( e ) ;
368
+ }
369
+ }
370
+ }
371
+
290
372
// TODO: copy vite:build-html plugin
291
373
function viterollEntryPlugin (
292
374
config : ResolvedConfig ,
293
375
viterollOptions : ViterollOptions ,
376
+ environment : RolldownEnvironment ,
294
377
) : rolldown . Plugin {
295
378
const htmlEntryMap = new Map < string , MagicString > ( ) ;
296
379
@@ -337,14 +420,27 @@ function viterollEntryPlugin(
337
420
if ( code . includes ( "//#region rolldown:runtime" ) ) {
338
421
const output = new MagicString ( code ) ;
339
422
// replace hard-coded WebSocket setup with custom one
340
- output . replace ( / c o n s t s o c k e t = .* ?\n } ; / s, getRolldownClientCode ( config ) ) ;
423
+ output . replace (
424
+ / c o n s t s o c k e t = .* ?\n } ; / s,
425
+ environment . name === "client" ? getRolldownClientCode ( config ) : "" ,
426
+ ) ;
341
427
// trigger full rebuild on non-accepting entry invalidation
342
428
output
429
+ . replace (
430
+ "this.executeModuleStack.length > 1" ,
431
+ "this.executeModuleStack.length >= 1" ,
432
+ )
343
433
. replace ( "parents: [parent]," , "parents: parent ? [parent] : []," )
434
+ . replace (
435
+ "if (module.parents.indexOf(parent) === -1) {" ,
436
+ "if (parent && module.parents.indexOf(parent) === -1) {" ,
437
+ )
344
438
. replace (
345
439
"for (var i = 0; i < module.parents.length; i++) {" ,
346
440
`
347
- if (module.parents.length === 0) {
441
+ boundaries.push(moduleId);
442
+ invalidModuleIds.push(moduleId);
443
+ if (module.parents.filter(Boolean).length === 0) {
348
444
__rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
349
445
break;
350
446
}
@@ -353,7 +449,10 @@ function viterollEntryPlugin(
353
449
if ( viterollOptions . reactRefresh ) {
354
450
output . prepend ( getReactRefreshRuntimeCode ( ) ) ;
355
451
}
356
- return { code : output . toString ( ) , map : output . generateMap ( ) } ;
452
+ return {
453
+ code : output . toString ( ) ,
454
+ map : output . generateMap ( { hires : "boundary" } ) ,
455
+ } ;
357
456
}
358
457
} ,
359
458
generateBundle ( _options , bundle ) {
0 commit comments