@@ -63,6 +63,7 @@ export function rolldownDevHandleConfig(
6363 createEnvironment : RolldownEnvironment . createFactory ( {
6464 hmr : config . experimental ?. rolldownDev ?. hmr ,
6565 reactRefresh : config . experimental ?. rolldownDev ?. reactRefresh ,
66+ ssrModuleRunner : false ,
6667 } ) ,
6768 } ,
6869 build : {
@@ -81,6 +82,7 @@ export function rolldownDevHandleConfig(
8182 createEnvironment : RolldownEnvironment . createFactory ( {
8283 hmr : false ,
8384 reactRefresh : false ,
85+ ssrModuleRunner : config . experimental ?. rolldownDev ?. ssrModuleRunner ,
8486 } ) ,
8587 } ,
8688 } ,
@@ -134,6 +136,8 @@ class RolldownEnvironment extends DevEnvironment {
134136 result ! : rolldown . RolldownOutput
135137 outDir ! : string
136138 buildTimestamp = Date . now ( )
139+ inputOptions ! : rolldown . InputOptions
140+ outputOptions ! : rolldown . OutputOptions
137141
138142 static createFactory (
139143 rolldownDevOptioins : RolldownDevOptions ,
@@ -200,7 +204,7 @@ class RolldownEnvironment extends DevEnvironment {
200204 plugins = plugins . map ( ( p ) => injectEnvironmentToHooks ( this as any , p ) )
201205
202206 console . time ( `[rolldown:${ this . name } :build]` )
203- const inputOptions : rolldown . InputOptions = {
207+ this . inputOptions = {
204208 dev : this . rolldownDevOptions . hmr ,
205209 input : this . config . build . rollupOptions . input ,
206210 cwd : this . config . root ,
@@ -212,30 +216,34 @@ class RolldownEnvironment extends DevEnvironment {
212216 } ,
213217 plugins : [
214218 ...plugins ,
215- patchRuntimePlugin ( this . rolldownDevOptions ) ,
219+ patchRuntimePlugin ( this ) ,
216220 patchCssPlugin ( ) ,
217221 reactRefreshPlugin ( ) ,
218222 ] ,
219223 moduleTypes : {
220224 '.css' : 'js' ,
221225 } ,
222226 }
223- this . instance = await rolldown . rolldown ( inputOptions )
227+ this . instance = await rolldown . rolldown ( this . inputOptions )
224228
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 = {
227234 dir : this . outDir ,
228- format : this . rolldownDevOptions . hmr ? 'app' : 'esm' ,
235+ format,
229236 // TODO: hmr_rebuild returns source map file when `sourcemap: true`
230237 sourcemap : 'inline' ,
231238 // TODO: https://github.com/rolldown/rolldown/issues/2041
232239 // handle `require("stream")` in `react-dom/server`
233240 banner :
234- this . name === 'ssr'
241+ this . name === 'ssr' && format === 'esm'
235242 ? `import __nodeModule from "node:module"; const require = __nodeModule.createRequire(import.meta.url);`
236243 : undefined ,
237244 }
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 )
239247
240248 this . buildTimestamp = Date . now ( )
241249 console . timeEnd ( `[rolldown:${ this . name } :build]` )
@@ -249,12 +257,19 @@ class RolldownEnvironment extends DevEnvironment {
249257 if ( ! output . moduleIds . includes ( ctx . file ) ) {
250258 return
251259 }
252- if ( this . rolldownDevOptions . hmr ) {
260+ if (
261+ this . rolldownDevOptions . hmr ||
262+ this . rolldownDevOptions . ssrModuleRunner
263+ ) {
253264 logger . info ( `hmr '${ ctx . file } '` , { timestamp : true } )
254265 console . time ( `[rolldown:${ this . name } :hmr]` )
255266 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+ }
256272 console . timeEnd ( `[rolldown:${ this . name } :hmr]` )
257- ctx . server . ws . send ( 'rolldown:hmr' , result )
258273 } else {
259274 await this . build ( )
260275 if ( this . name === 'client' ) {
@@ -263,40 +278,136 @@ class RolldownEnvironment extends DevEnvironment {
263278 }
264279 }
265280
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+
266294 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 ]
269300 const filepath = path . join ( this . outDir , output . fileName )
270301 return import ( `${ pathToFileURL ( filepath ) } ?t=${ this . buildTimestamp } ` )
271302 }
272303}
273304
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 {
277356 return {
278357 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+ } ,
279377 renderChunk ( code ) {
280378 // patch rolldown_runtime to workaround a few things
281379 // TODO: is there a robust way to inject code specifically to entry or runtime?
282380 if ( code . includes ( '//#region rolldown:runtime' ) ) {
283- // TODO: is this magic string heavy?
381+ // TODO: this magic string is heavy
284382 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
288383 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+ )
289394 . replace ( 'parents: [parent],' , 'parents: parent ? [parent] : [],' )
395+ . replace (
396+ 'if (module.parents.indexOf(parent) === -1) {' ,
397+ 'if (parent && module.parents.indexOf(parent) === -1) {' ,
398+ )
290399 . replace (
291400 'for (var i = 0; i < module.parents.length; i++) {' ,
292401 `
293- if (module.parents.length === 0) {
402+ boundaries.push(moduleId);
403+ invalidModuleIds.push(moduleId);
404+ if (module.parents.filter(Boolean).length === 0) {
294405 __rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
295406 break;
296407 }
297408 for (var i = 0; i < module.parents.length; i++) {` ,
298409 )
299- if ( rolldownDevOptions . reactRefresh ) {
410+ if ( environment . rolldownDevOptions . reactRefresh ) {
300411 output . prepend ( getReactRefreshRuntimeCode ( ) )
301412 }
302413 return {
0 commit comments