@@ -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,22 @@ 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 (
271+ result [ 1 ] . toString ( ) ,
272+ path . join ( this . outDir , result [ 0 ] ) ,
273+ )
274+ }
256275 console . timeEnd ( `[rolldown:${ this . name } :hmr]` )
257- ctx . server . ws . send ( 'rolldown:hmr' , result )
258276 } else {
259277 await this . build ( )
260278 if ( this . name === 'client' ) {
@@ -263,40 +281,138 @@ class RolldownEnvironment extends DevEnvironment {
263281 }
264282 }
265283
284+ runner ! : RolldownModuleRunner
285+
286+ getRunner ( ) {
287+ if ( ! this . runner ) {
288+ const output = this . result . output [ 0 ]
289+ const filepath = path . join ( this . outDir , output . fileName )
290+ this . runner = new RolldownModuleRunner ( )
291+ const code = fs . readFileSync ( filepath , 'utf-8' )
292+ this . runner . evaluate ( code , filepath )
293+ }
294+ return this . runner
295+ }
296+
266297 async import ( input : string ) : Promise < unknown > {
267- const output = this . result . output . find ( ( o ) => o . name === input )
268- assert ( output , `invalid import input '${ input } '` )
298+ if ( this . outputOptions . format === 'app' ) {
299+ return this . getRunner ( ) . import ( input )
300+ }
301+ // input is no use
302+ const output = this . result . output [ 0 ]
269303 const filepath = path . join ( this . outDir , output . fileName )
304+ // TODO: source map not applied when adding `?t=...`?
305+ // return import(`${pathToFileURL(filepath)}`)
270306 return import ( `${ pathToFileURL ( filepath ) } ?t=${ this . buildTimestamp } ` )
271307 }
272308}
273309
274- function patchRuntimePlugin (
275- rolldownDevOptions : RolldownDevOptions ,
276- ) : rolldown . Plugin {
310+ class RolldownModuleRunner {
311+ // intercept globals
312+ private context = {
313+ rolldown_runtime : { } as any ,
314+ __rolldown_hot : {
315+ send : ( ) => { } ,
316+ } ,
317+ // TODO: external require doesn't work in app format.
318+ // TODO: also it should be aware of importer for non static require/import.
319+ _require : require ,
320+ }
321+
322+ // TODO: support resolution?
323+ async import ( id : string ) : Promise < unknown > {
324+ const mod = this . context . rolldown_runtime . moduleCache [ id ]
325+ assert ( mod , `Module not found '${ id } '` )
326+ return mod . exports
327+ }
328+
329+ evaluate ( code : string , sourceURL : string ) {
330+ const context = {
331+ self : this . context ,
332+ ...this . context ,
333+ }
334+ // extract sourcemap and move to the bottom
335+ const sourcemap = code . match ( / ^ \/ \/ # s o u r c e M a p p i n g U R L = .* / m) ?. [ 0 ] ?? ''
336+ if ( sourcemap ) {
337+ code = code . replace ( sourcemap , '' )
338+ }
339+ code = `\
340+ 'use strict';(${ Object . keys ( context ) . join ( ',' ) } )=>{{${ code }
341+ // TODO: need to re-expose runtime utilities for now
342+ self.__toCommonJS = __toCommonJS;
343+ self.__export = __export;
344+ self.__toESM = __toESM;
345+ }}
346+ //# sourceURL=${ sourceURL }
347+ //# sourceMappingSource=rolldown-module-runner
348+ ${ sourcemap }
349+ `
350+ const fn = ( 0 , eval ) ( code )
351+ try {
352+ fn ( ...Object . values ( context ) )
353+ } catch ( e ) {
354+ console . error ( '[RolldownModuleRunner:ERROR]' , e )
355+ throw e
356+ }
357+ }
358+ }
359+
360+ function patchRuntimePlugin ( environment : RolldownEnvironment ) : rolldown . Plugin {
277361 return {
278362 name : 'vite:rolldown-patch-runtime' ,
363+ // TODO: external require doesn't work in app format.
364+ // rewrite `require -> _require` and provide _require from module runner.
365+ // for now just rewrite known ones in "react-dom/server".
366+ transform : {
367+ filter : {
368+ code : {
369+ include : [ / r e q u i r e \( [ ' " ] ( s t r e a m | u t i l ) [ ' " ] \) / ] ,
370+ } ,
371+ } ,
372+ handler ( code ) {
373+ if ( ! environment . rolldownDevOptions . ssrModuleRunner ) {
374+ return
375+ }
376+ return code . replace (
377+ / r e q u i r e ( \( [ ' " ] ( s t r e a m | u t i l ) [ ' " ] \) ) / g,
378+ '_require($1)' ,
379+ )
380+ } ,
381+ } ,
279382 renderChunk ( code ) {
280383 // patch rolldown_runtime to workaround a few things
281384 // TODO: is there a robust way to inject code specifically to entry or runtime?
282385 if ( code . includes ( '//#region rolldown:runtime' ) ) {
283- // TODO: is this magic string heavy?
386+ // TODO: this magic string is heavy
284387 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
288388 output
389+ // replace hard-coded WebSocket setup with custom client
390+ . replace (
391+ / c o n s t s o c k e t = .* ?\n \} ; / s,
392+ environment . name === 'client' ? getRolldownClientCode ( ) : '' ,
393+ )
394+ // fix rolldown_runtime.patch
395+ . replace (
396+ 'this.executeModuleStack.length > 1' ,
397+ 'this.executeModuleStack.length > 0' ,
398+ )
289399 . replace ( 'parents: [parent],' , 'parents: parent ? [parent] : [],' )
400+ . replace (
401+ 'if (module.parents.indexOf(parent) === -1) {' ,
402+ 'if (parent && module.parents.indexOf(parent) === -1) {' ,
403+ )
290404 . replace (
291405 'for (var i = 0; i < module.parents.length; i++) {' ,
292406 `
293- if (module.parents.length === 0) {
407+ boundaries.push(moduleId);
408+ invalidModuleIds.push(moduleId);
409+ if (module.parents.filter(Boolean).length === 0) {
294410 __rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
295411 break;
296412 }
297413 for (var i = 0; i < module.parents.length; i++) {` ,
298414 )
299- if ( rolldownDevOptions . reactRefresh ) {
415+ if ( environment . rolldownDevOptions . reactRefresh ) {
300416 output . prepend ( getReactRefreshRuntimeCode ( ) )
301417 }
302418 return {
0 commit comments