@@ -698,135 +698,146 @@ class ConsumeSharedPlugin {
698698 ) ;
699699
700700 // AFTER RESOLVE: alias-aware equality (single-resolution per candidate via cache)
701- normalModuleFactory . hooks . afterResolve . tapPromise (
702- PLUGIN_NAME ,
703- async ( data : any /* ResolveData-like */ ) => {
704- await promise ;
705-
706- const dependencies = data . dependencies as any [ ] ;
707- if (
708- dependencies &&
709- ( dependencies [ 0 ] instanceof ConsumeSharedFallbackDependency ||
710- dependencies [ 0 ] instanceof ProvideForSharedDependency )
711- ) {
712- return ;
713- }
701+ {
702+ const afterResolveHook = ( normalModuleFactory as any ) ?. hooks
703+ ?. afterResolve ;
704+ if ( afterResolveHook ?. tapPromise ) {
705+ afterResolveHook . tapPromise (
706+ PLUGIN_NAME ,
707+ async ( data : any /* ResolveData-like */ ) => {
708+ await promise ;
709+
710+ const dependencies = data . dependencies as any [ ] ;
711+ if (
712+ dependencies &&
713+ ( dependencies [ 0 ] instanceof ConsumeSharedFallbackDependency ||
714+ dependencies [ 0 ] instanceof ProvideForSharedDependency )
715+ ) {
716+ return ;
717+ }
714718
715- const createData = data . createData || data ;
716- const resource : string | undefined =
717- createData && createData . resource ;
718- if ( ! resource ) return ;
719- // Skip virtual/data URI resources – let webpack handle them
720- if ( resource . startsWith ( 'data:' ) ) return ;
721- // Do not convert explicit relative/absolute path requests into consumes
722- // e.g. "./node_modules/shared" inside a package should resolve locally
723- const originalRequest : string | undefined = data . request ;
724- if (
725- originalRequest &&
726- RELATIVE_OR_ABSOLUTE_PATH_REGEX . test ( originalRequest )
727- ) {
728- return ;
729- }
730- if ( resolvedConsumes . has ( resource ) ) return ;
719+ const createData = data . createData || data ;
720+ const resource : string | undefined =
721+ createData && createData . resource ;
722+ if ( ! resource ) return ;
723+ // Skip virtual/data URI resources – let webpack handle them
724+ if ( resource . startsWith ( 'data:' ) ) return ;
725+ // Do not convert explicit relative/absolute path requests into consumes
726+ // e.g. "./node_modules/shared" inside a package should resolve locally
727+ const originalRequest : string | undefined = data . request ;
728+ if (
729+ originalRequest &&
730+ RELATIVE_OR_ABSOLUTE_PATH_REGEX . test ( originalRequest )
731+ ) {
732+ return ;
733+ }
734+ if ( resolvedConsumes . has ( resource ) ) return ;
731735
732- const issuerLayer : string | undefined =
733- data . contextInfo && data . contextInfo . issuerLayer === null
734- ? undefined
735- : data . contextInfo ?. issuerLayer ;
736+ const issuerLayer : string | undefined =
737+ data . contextInfo && data . contextInfo . issuerLayer === null
738+ ? undefined
739+ : data . contextInfo ?. issuerLayer ;
736740
737- // Try to get the package name via resolver metadata first
738- let pkgName : string | undefined =
739- createData ?. resourceResolveData ?. descriptionFileData ?. name ;
741+ // Try to get the package name via resolver metadata first
742+ let pkgName : string | undefined =
743+ createData ?. resourceResolveData ?. descriptionFileData ?. name ;
740744
741- if ( ! pkgName ) {
742- pkgName = await getPackageNameForResource ( resource ) ;
743- }
744- if ( ! pkgName ) return ;
745-
746- // Candidate configs: include
747- // - exact package name keys (legacy behavior)
748- // - deep-path shares whose keys start with `${pkgName}/` (alias-aware)
749- const candidates : ConsumeOptions [ ] = [ ] ;
750- const seen = new Set < ConsumeOptions > ( ) ;
751- const k1 = createLookupKeyForSharing ( pkgName , issuerLayer ) ;
752- const k2 = createLookupKeyForSharing ( pkgName , undefined ) ;
753- const c1 = unresolvedConsumes . get ( k1 ) ;
754- const c2 = unresolvedConsumes . get ( k2 ) ;
755- if ( c1 && ! seen . has ( c1 ) ) {
756- candidates . push ( c1 ) ;
757- seen . add ( c1 ) ;
758- }
759- if ( c2 && ! seen . has ( c2 ) ) {
760- candidates . push ( c2 ) ;
761- seen . add ( c2 ) ;
762- }
745+ if ( ! pkgName ) {
746+ pkgName = await getPackageNameForResource ( resource ) ;
747+ }
748+ if ( ! pkgName ) return ;
749+
750+ // Candidate configs: include
751+ // - exact package name keys (legacy behavior)
752+ // - deep-path shares whose keys start with `${pkgName}/` (alias-aware)
753+ const candidates : ConsumeOptions [ ] = [ ] ;
754+ const seen = new Set < ConsumeOptions > ( ) ;
755+ const k1 = createLookupKeyForSharing ( pkgName , issuerLayer ) ;
756+ const k2 = createLookupKeyForSharing ( pkgName , undefined ) ;
757+ const c1 = unresolvedConsumes . get ( k1 ) ;
758+ const c2 = unresolvedConsumes . get ( k2 ) ;
759+ if ( c1 && ! seen . has ( c1 ) ) {
760+ candidates . push ( c1 ) ;
761+ seen . add ( c1 ) ;
762+ }
763+ if ( c2 && ! seen . has ( c2 ) ) {
764+ candidates . push ( c2 ) ;
765+ seen . add ( c2 ) ;
766+ }
763767
764- // Also scan for deep-path keys beginning with `${pkgName}/` (both layered and unlayered)
765- const prefixLayered = createLookupKeyForSharing (
766- pkgName + '/' ,
767- issuerLayer ,
768- ) ;
769- const prefixUnlayered = createLookupKeyForSharing (
770- pkgName + '/' ,
771- undefined ,
768+ // Also scan for deep-path keys beginning with `${pkgName}/` (both layered and unlayered)
769+ const prefixLayered = createLookupKeyForSharing (
770+ pkgName + '/' ,
771+ issuerLayer ,
772+ ) ;
773+ const prefixUnlayered = createLookupKeyForSharing (
774+ pkgName + '/' ,
775+ undefined ,
776+ ) ;
777+ for ( const [ key , cfg ] of unresolvedConsumes ) {
778+ if (
779+ ( key . startsWith ( prefixLayered ) ||
780+ key . startsWith ( prefixUnlayered ) ) &&
781+ ! seen . has ( cfg )
782+ ) {
783+ candidates . push ( cfg ) ;
784+ seen . add ( cfg ) ;
785+ }
786+ }
787+ if ( candidates . length === 0 ) return ;
788+
789+ // Build resolver aligned with current resolve context
790+ const baseResolver = compilation . resolverFactory . get ( 'normal' , {
791+ dependencyType : data . dependencyType || 'esm' ,
792+ } as ResolveOptionsWithDependencyType ) ;
793+ const resolver =
794+ data . resolveOptions &&
795+ typeof ( baseResolver as any ) . withOptions === 'function'
796+ ? ( baseResolver as any ) . withOptions ( data . resolveOptions )
797+ : data . resolveOptions
798+ ? compilation . resolverFactory . get (
799+ 'normal' ,
800+ Object . assign (
801+ {
802+ dependencyType : data . dependencyType || 'esm' ,
803+ } ,
804+ data . resolveOptions ,
805+ ) as ResolveOptionsWithDependencyType ,
806+ )
807+ : ( baseResolver as any ) ;
808+
809+ const resolverKey = JSON . stringify ( {
810+ dependencyType : data . dependencyType || 'esm' ,
811+ resolveOptions : data . resolveOptions || null ,
812+ } ) ;
813+ const ctx =
814+ createData ?. context ||
815+ data . context ||
816+ compilation . compiler . context ;
817+
818+ // Resolve each candidate's target once, compare by absolute path
819+ for ( const cfg of candidates ) {
820+ const targetReq = ( cfg . request || cfg . import ) as string ;
821+ const targetResolved = await resolveOnce (
822+ resolver ,
823+ ctx ,
824+ targetReq ,
825+ resolverKey ,
826+ ) ;
827+ if ( targetResolved && targetResolved === resource ) {
828+ resolvedConsumes . set ( resource , cfg ) ;
829+ break ;
830+ }
831+ }
832+ } ,
772833 ) ;
773- for ( const [ key , cfg ] of unresolvedConsumes ) {
774- if (
775- ( key . startsWith ( prefixLayered ) ||
776- key . startsWith ( prefixUnlayered ) ) &&
777- ! seen . has ( cfg )
778- ) {
779- candidates . push ( cfg ) ;
780- seen . add ( cfg ) ;
781- }
782- }
783- if ( candidates . length === 0 ) return ;
784-
785- // Build resolver aligned with current resolve context
786- const baseResolver = compilation . resolverFactory . get ( 'normal' , {
787- dependencyType : data . dependencyType || 'esm' ,
788- } as ResolveOptionsWithDependencyType ) ;
789- const resolver =
790- data . resolveOptions &&
791- typeof ( baseResolver as any ) . withOptions === 'function'
792- ? ( baseResolver as any ) . withOptions ( data . resolveOptions )
793- : data . resolveOptions
794- ? compilation . resolverFactory . get (
795- 'normal' ,
796- Object . assign (
797- {
798- dependencyType : data . dependencyType || 'esm' ,
799- } ,
800- data . resolveOptions ,
801- ) as ResolveOptionsWithDependencyType ,
802- )
803- : ( baseResolver as any ) ;
804-
805- const resolverKey = JSON . stringify ( {
806- dependencyType : data . dependencyType || 'esm' ,
807- resolveOptions : data . resolveOptions || null ,
834+ } else if ( afterResolveHook ?. tap ) {
835+ // Fallback for tests/mocks that only expose sync hooks to avoid throw
836+ afterResolveHook . tap ( PLUGIN_NAME , ( _data : any ) => {
837+ // no-op in sync mock environments; this avoids throwing during plugin registration
808838 } ) ;
809- const ctx =
810- createData ?. context ||
811- data . context ||
812- compilation . compiler . context ;
813-
814- // Resolve each candidate's target once, compare by absolute path
815- for ( const cfg of candidates ) {
816- const targetReq = ( cfg . request || cfg . import ) as string ;
817- const targetResolved = await resolveOnce (
818- resolver ,
819- ctx ,
820- targetReq ,
821- resolverKey ,
822- ) ;
823- if ( targetResolved && targetResolved === resource ) {
824- resolvedConsumes . set ( resource , cfg ) ;
825- break ;
826- }
827- }
828- } ,
829- ) ;
839+ }
840+ }
830841
831842 // CREATE MODULE: swap resolved resource with ConsumeSharedModule when mapped
832843 normalModuleFactory . hooks . createModule . tapPromise (
@@ -858,51 +869,47 @@ class ConsumeSharedPlugin {
858869 ) ;
859870
860871 // Add finishModules hook to copy buildMeta/buildInfo from fallback modules *after* webpack's export analysis
861- // Running earlier causes failures, so we intentionally execute later than plugins like FlagDependencyExportsPlugin.
862- // This still follows webpack's pattern used by FlagDependencyExportsPlugin and InferAsyncModulesPlugin, but with a
863- // later stage. Based on webpack's Compilation.js: finishModules (line 2833) runs before seal (line 2920).
864- compilation . hooks . finishModules . tapAsync (
865- {
866- name : PLUGIN_NAME ,
867- stage : 10 , // Run after FlagDependencyExportsPlugin (default stage 0)
868- } ,
869- ( modules , callback ) => {
870- for ( const module of modules ) {
871- // Only process ConsumeSharedModule instances with fallback dependencies
872- if (
873- ! ( module instanceof ConsumeSharedModule ) ||
874- ! module . options . import
875- ) {
876- continue ;
877- }
878-
879- let dependency ;
880- if ( module . options . eager ) {
881- // For eager mode, get the fallback directly from dependencies
882- dependency = module . dependencies [ 0 ] ;
883- } else {
884- // For async mode, get it from the async dependencies block
885- dependency = module . blocks [ 0 ] ?. dependencies [ 0 ] ;
886- }
887-
888- if ( dependency ) {
889- const fallbackModule =
890- compilation . moduleGraph . getModule ( dependency ) ;
872+ // Guard for test environments where hooks may be lightly stubbed
873+ if ( compilation . hooks ?. finishModules ?. tapAsync ) {
874+ compilation . hooks . finishModules . tapAsync (
875+ {
876+ name : PLUGIN_NAME ,
877+ stage : 10 , // Run after FlagDependencyExportsPlugin (default stage 0)
878+ } ,
879+ ( modules , callback ) => {
880+ for ( const module of modules ) {
881+ // Only process ConsumeSharedModule instances with fallback dependencies
891882 if (
892- fallbackModule &&
893- fallbackModule . buildMeta &&
894- fallbackModule . buildInfo
883+ ! ( module instanceof ConsumeSharedModule ) ||
884+ ! module . options . import
895885 ) {
896- // Copy buildMeta and buildInfo following webpack's DelegatedModule pattern: this.buildMeta = { ...delegateData.buildMeta };
897- // This ensures ConsumeSharedModule inherits ESM/CJS detection (exportsType) and other optimization metadata
898- module . buildMeta = { ...fallbackModule . buildMeta } ;
899- module . buildInfo = { ...fallbackModule . buildInfo } ;
886+ continue ;
887+ }
888+
889+ let dependency ;
890+ if ( module . options . eager ) {
891+ dependency = module . dependencies [ 0 ] ;
892+ } else {
893+ dependency = module . blocks [ 0 ] ?. dependencies [ 0 ] ;
894+ }
895+
896+ if ( dependency ) {
897+ const fallbackModule =
898+ compilation . moduleGraph . getModule ( dependency ) ;
899+ if (
900+ fallbackModule &&
901+ fallbackModule . buildMeta &&
902+ fallbackModule . buildInfo
903+ ) {
904+ module . buildMeta = { ...fallbackModule . buildMeta } ;
905+ module . buildInfo = { ...fallbackModule . buildInfo } ;
906+ }
900907 }
901908 }
902- }
903- callback ( ) ;
904- } ,
905- ) ;
909+ callback ( ) ;
910+ } ,
911+ ) ;
912+ }
906913
907914 compilation . hooks . additionalTreeRuntimeRequirements . tap (
908915 PLUGIN_NAME ,
0 commit comments