@@ -69,11 +69,11 @@ const RSC_CLIENT_CALLBACK_BOOTSTRAP_MODULE = resolveFirstExistingPath(
6969 path . resolve ( __dirname , '../esm/runtime/rsc-client-callback-bootstrap.mjs' ) ,
7070 path . resolve (
7171 path . dirname ( RSC_BRIDGE_EXPOSE_MODULE ) ,
72- '../../esm/runtime/ rsc-client-callback-bootstrap.mjs ' ,
72+ 'rsc-client-callback-bootstrap.js ' ,
7373 ) ,
7474 path . resolve (
7575 path . dirname ( RSC_BRIDGE_EXPOSE_MODULE ) ,
76- '../../cjs/runtime/ rsc-client-callback-bootstrap.js ' ,
76+ 'rsc-client-callback-bootstrap.mjs ' ,
7777 ) ,
7878 ] ,
7979 path . resolve ( __dirname , '../runtime/rsc-client-callback-bootstrap.js' ) ,
@@ -285,7 +285,7 @@ const setRscExposeConfig = (
285285 const importList = Array . isArray ( normalizedConfig . import )
286286 ? [ ...normalizedConfig . import ]
287287 : [ normalizedConfig . import ] ;
288- const normalizedImport =
288+ const normalizedImportList =
289289 exposeKey === RSC_BRIDGE_EXPOSE
290290 ? importList
291291 : [
@@ -294,7 +294,10 @@ const setRscExposeConfig = (
294294 importPath => importPath !== RSC_CLIENT_CALLBACK_BOOTSTRAP_MODULE ,
295295 ) ,
296296 ] ;
297-
297+ const normalizedImport =
298+ normalizedImportList . length === 1
299+ ? normalizedImportList [ 0 ]
300+ : normalizedImportList ;
298301 normalizedExposes [ exposeKey ] = {
299302 ...normalizedConfig ,
300303 import : normalizedImport ,
@@ -450,10 +453,7 @@ export const patchMFConfig = (
450453 ) ;
451454 }
452455
453- if (
454- rscEnabled &&
455- ( hasRemotes ( mfConfig . remotes ) || hasExposes ( mfConfig . exposes ) )
456- ) {
456+ if ( rscEnabled && hasRemotes ( mfConfig . remotes ) ) {
457457 injectRuntimePlugins ( RSC_BRIDGE_RUNTIME_PLUGIN , runtimePlugins ) ;
458458 }
459459
@@ -528,6 +528,177 @@ function patchIgnoreWarning(chain: BundlerChainConfig) {
528528 chain . ignoreWarnings ( ignoreWarnings ) ;
529529}
530530
531+ const patchProjectNodeModulesResolution = ( chain : BundlerChainConfig ) => {
532+ // Keep federation + RSC resolution rooted in the app workspace to avoid
533+ // divergent hoisted dependency paths between client/server manifests.
534+ const projectNodeModulesPath = path . resolve ( process . cwd ( ) , 'node_modules' ) ;
535+ if ( ! fs . existsSync ( projectNodeModulesPath ) ) {
536+ return ;
537+ }
538+
539+ const resolveModules = chain . resolve . modules as {
540+ values ?: ( ) => string [ ] ;
541+ clear : ( ) => unknown ;
542+ add : ( value : string ) => unknown ;
543+ } ;
544+ resolveModules . clear ( ) ;
545+ resolveModules . add ( projectNodeModulesPath ) ;
546+ resolveModules . add ( 'node_modules' ) ;
547+ } ;
548+
549+ const patchServerOnlyAlias = ( chain : BundlerChainConfig ) => {
550+ // Align server-only package behavior for federated remote exposes across
551+ // both build targets.
552+ const serverOnlyEmptyPath = path . resolve (
553+ process . cwd ( ) ,
554+ 'node_modules/server-only/empty.js' ,
555+ ) ;
556+ if ( ! fs . existsSync ( serverOnlyEmptyPath ) ) {
557+ return ;
558+ }
559+
560+ const aliasChain = chain . resolve . alias as {
561+ has ?: ( key : string ) => boolean ;
562+ set : ( key : string , value : string ) => unknown ;
563+ } ;
564+ const hasServerOnlyAlias =
565+ typeof aliasChain . has === 'function' ? aliasChain . has ( 'server-only$' ) : false ;
566+ if ( ! hasServerOnlyAlias ) {
567+ aliasChain . set ( 'server-only$' , serverOnlyEmptyPath ) ;
568+ }
569+ } ;
570+
571+ const resolveProjectDependency = ( request : string ) => {
572+ try {
573+ return require . resolve ( request , { paths : [ process . cwd ( ) ] } ) ;
574+ } catch {
575+ try {
576+ return require . resolve ( request ) ;
577+ } catch {
578+ return undefined ;
579+ }
580+ }
581+ } ;
582+
583+ const patchRscServerRuntimeAliases = ( chain : BundlerChainConfig ) => {
584+ const reactPackagePath = resolveProjectDependency ( 'react/package.json' ) ;
585+ if ( ! reactPackagePath ) {
586+ return ;
587+ }
588+
589+ const reactDir = path . dirname ( reactPackagePath ) ;
590+ const reactJsxRuntimeServerPath = path . join (
591+ reactDir ,
592+ 'jsx-runtime.react-server.js' ,
593+ ) ;
594+ const reactJsxDevRuntimeServerPath = path . join (
595+ reactDir ,
596+ 'jsx-dev-runtime.react-server.js' ,
597+ ) ;
598+
599+ if ( fs . existsSync ( reactJsxRuntimeServerPath ) ) {
600+ chain . resolve . alias . set ( 'react/jsx-runtime$' , reactJsxRuntimeServerPath ) ;
601+ }
602+ if ( fs . existsSync ( reactJsxDevRuntimeServerPath ) ) {
603+ chain . resolve . alias . set (
604+ 'react/jsx-dev-runtime$' ,
605+ reactJsxDevRuntimeServerPath ,
606+ ) ;
607+ }
608+ } ;
609+
610+ const getExposeImports = (
611+ exposeConfig : moduleFederationPlugin . ExposesObject [ string ] ,
612+ ) : string [ ] => {
613+ if ( typeof exposeConfig === 'string' ) {
614+ return [ exposeConfig ] ;
615+ }
616+ if ( Array . isArray ( exposeConfig ) ) {
617+ return exposeConfig . filter (
618+ ( importPath ) : importPath is string => typeof importPath === 'string' ,
619+ ) ;
620+ }
621+ if (
622+ exposeConfig &&
623+ typeof exposeConfig === 'object' &&
624+ 'import' in exposeConfig
625+ ) {
626+ const exposeImport = ( exposeConfig as { import ?: unknown } ) . import ;
627+ if ( typeof exposeImport === 'string' ) {
628+ return [ exposeImport ] ;
629+ }
630+ if ( Array . isArray ( exposeImport ) ) {
631+ return exposeImport . filter (
632+ ( importPath ) : importPath is string => typeof importPath === 'string' ,
633+ ) ;
634+ }
635+ }
636+ return [ ] ;
637+ } ;
638+
639+ const collectExposeImportDirectories = (
640+ exposes : moduleFederationPlugin . ModuleFederationPluginOptions [ 'exposes' ] ,
641+ ) => {
642+ if ( ! exposes ) {
643+ return [ ] ;
644+ }
645+
646+ const exposeEntries = Array . isArray ( exposes )
647+ ? exposes . flatMap ( exposeItem =>
648+ typeof exposeItem === 'string'
649+ ? [ [ exposeItem , exposeItem ] as const ]
650+ : ( Object . entries ( exposeItem || { } ) as Array <
651+ readonly [
652+ string ,
653+ moduleFederationPlugin . ExposesObject [ string ] ,
654+ ]
655+ > ) ,
656+ )
657+ : ( Object . entries ( exposes ) as Array <
658+ readonly [ string , moduleFederationPlugin . ExposesObject [ string ] ]
659+ > ) ;
660+
661+ const directories = new Set < string > ( ) ;
662+ for ( const [ exposeKey , exposeConfig ] of exposeEntries ) {
663+ if ( exposeKey === RSC_BRIDGE_EXPOSE ) {
664+ continue ;
665+ }
666+ for ( const importPath of getExposeImports ( exposeConfig ) ) {
667+ if ( ! importPath || ! importPath . startsWith ( '.' ) ) {
668+ continue ;
669+ }
670+ directories . add ( path . dirname ( path . resolve ( process . cwd ( ) , importPath ) ) ) ;
671+ }
672+ }
673+
674+ return Array . from ( directories ) ;
675+ } ;
676+
677+ const patchRscRemoteComponentLayer = (
678+ chain : BundlerChainConfig ,
679+ mfConfig : moduleFederationPlugin . ModuleFederationPluginOptions ,
680+ ) => {
681+ // Derive layer coverage from actual expose imports instead of fixture-specific
682+ // source path conventions.
683+ const includeDirectories = collectExposeImportDirectories ( mfConfig . exposes ) ;
684+ if ( includeDirectories . length === 0 ) {
685+ return ;
686+ }
687+
688+ const ruleChain = chain . module
689+ . rule ( 'rsc-mf-remote-components-layer' )
690+ . test ( / \. [ c m ] ? [ j t ] s x ? $ / ) ;
691+
692+ for ( const includeDirectory of includeDirectories ) {
693+ ruleChain . include . add ( includeDirectory ) ;
694+ }
695+
696+ ruleChain . layer ( RSC_LAYER ) ;
697+ } ;
698+
699+ const normalizePublicPath = ( publicPath : string ) =>
700+ publicPath . endsWith ( '/' ) ? publicPath . slice ( 0 , - 1 ) : publicPath ;
701+
531702export function addMyTypes2Ignored (
532703 chain : BundlerChainConfig ,
533704 mfConfig : moduleFederationPlugin . ModuleFederationPluginOptions ,
@@ -588,11 +759,44 @@ export function patchBundlerConfig(options: {
588759 enableSSR : boolean ;
589760} ) {
590761 const { chain, modernjsConfig, isServer, mfConfig, enableSSR } = options ;
762+ const rscMfEnabled = isRscMfEnabled ( mfConfig ) ;
591763
592764 chain . optimization . delete ( 'runtimeChunk' ) ;
593765
594766 patchIgnoreWarning ( chain ) ;
595767
768+ if ( rscMfEnabled && isServer ) {
769+ chain . resolve . conditionNames
770+ . clear ( )
771+ . add ( 'require' )
772+ . add ( 'import' )
773+ . add ( 'default' ) ;
774+ if ( hasExposes ( mfConfig . exposes ) ) {
775+ chain . resolve . conditionNames . add ( 'react-server' ) ;
776+ }
777+ }
778+
779+ if ( rscMfEnabled && hasExposes ( mfConfig . exposes ) ) {
780+ patchProjectNodeModulesResolution ( chain ) ;
781+ patchServerOnlyAlias ( chain ) ;
782+
783+ const assetPrefix = modernjsConfig . output ?. assetPrefix ;
784+ if ( typeof assetPrefix === 'string' && assetPrefix . trim ( ) ) {
785+ const normalizedAssetPrefix = normalizePublicPath ( assetPrefix . trim ( ) ) ;
786+ chain . output . publicPath (
787+ isServer
788+ ? `${ normalizedAssetPrefix } /bundles/`
789+ : `${ normalizedAssetPrefix } /` ,
790+ ) ;
791+ }
792+ if ( ! isServer ) {
793+ chain . optimization . splitChunks ( false ) ;
794+ } else {
795+ patchRscServerRuntimeAliases ( chain ) ;
796+ patchRscRemoteComponentLayer ( chain , mfConfig ) ;
797+ }
798+ }
799+
596800 if ( ! chain . output . get ( 'chunkLoadingGlobal' ) ) {
597801 chain . output . chunkLoadingGlobal ( `chunk_${ mfConfig . name } ` ) ;
598802 }
0 commit comments