@@ -47,6 +47,23 @@ describe('ConsumeSharedPlugin', () => {
47
47
path . join ( nodeModulesDir , 'react/index.js' ) ,
48
48
'module.exports = { version: "17.0.2" };' ,
49
49
) ;
50
+
51
+ // Add a project-level package.json to testDir
52
+ fs . writeFileSync (
53
+ path . join ( testDir , 'package.json' ) ,
54
+ JSON . stringify ( {
55
+ name : 'test-nested-include' ,
56
+ version : '1.0.0' ,
57
+ dependencies : {
58
+ react : '16.8.0' ,
59
+ 'some-package' : '1.0.0' ,
60
+ } ,
61
+ devDependencies : {
62
+ jest : '^29.0.0' ,
63
+ webpack : '^5.0.0' ,
64
+ } ,
65
+ } , null , 2 ) ,
66
+ ) ;
50
67
} ) ;
51
68
52
69
afterEach ( ( ) => {
@@ -264,6 +281,18 @@ describe('ConsumeSharedPlugin', () => {
264
281
'module.exports = { version: "16.8.0" };' ,
265
282
) ;
266
283
284
+ // Create a root package.json for the test project with react dependency
285
+ fs . writeFileSync (
286
+ path . join ( testDir , 'package.json' ) ,
287
+ JSON . stringify ( {
288
+ name : 'test-project' ,
289
+ version : '1.0.0' ,
290
+ dependencies : {
291
+ react : '16.8.0' ,
292
+ } ,
293
+ } , null , 2 ) ,
294
+ ) ;
295
+
267
296
// Create entry file
268
297
fs . writeFileSync (
269
298
path . join ( srcDir , 'index.js' ) ,
@@ -295,6 +324,7 @@ describe('ConsumeSharedPlugin', () => {
295
324
import : 'react' ,
296
325
shareKey : 'react' ,
297
326
shareScope : 'default' ,
327
+ requiredVersion : '^16.0.0' , // Explicitly set requiredVersion
298
328
exclude : {
299
329
version : '^16.0.0' , // Should exclude React 16.x.x
300
330
} ,
@@ -309,8 +339,9 @@ describe('ConsumeSharedPlugin', () => {
309
339
const stats = await compile ( compiler ) ;
310
340
311
341
expect ( stats . hasErrors ( ) ) . toBe ( false ) ;
342
+ expect ( stats . hasWarnings ( ) ) . toBe ( false ) ; // Assert no warnings
312
343
313
- const output = stats . toJson ( { modules : true } ) ;
344
+ const output = stats . toJson ( ) ;
314
345
const consumeSharedModule = output . modules ?. find (
315
346
( m ) =>
316
347
m . moduleType === 'consume-shared-module' &&
@@ -546,7 +577,6 @@ describe('ConsumeSharedPlugin', () => {
546
577
new ConsumeSharedPlugin ( {
547
578
consumes : {
548
579
react : {
549
- import : 'react' ,
550
580
shareKey : 'react' ,
551
581
shareScope : 'default' ,
552
582
requiredVersion : false , // Allow any version
@@ -648,4 +678,317 @@ describe('ConsumeSharedPlugin', () => {
648
678
// Module should be excluded since version matches exclude pattern
649
679
expect ( consumeSharedModule ) . toBeUndefined ( ) ;
650
680
} ) ;
681
+
682
+ describe ( 'include functionality' , ( ) => {
683
+ describe ( 'version-based inclusion' , ( ) => {
684
+ it ( 'should include module when version matches include.version' , async ( ) => {
685
+ // Setup React v17.0.2 which should be included
686
+ fs . writeFileSync (
687
+ path . join ( nodeModulesDir , 'react/package.json' ) ,
688
+ JSON . stringify ( {
689
+ name : 'react' ,
690
+ version : '17.0.2' ,
691
+ } ) ,
692
+ ) ;
693
+ fs . writeFileSync (
694
+ path . join ( nodeModulesDir , 'react/index.js' ) ,
695
+ 'module.exports = { version: "17.0.2" };' ,
696
+ ) ;
697
+
698
+ // Create a root package.json for the test project with react dependency
699
+ fs . writeFileSync (
700
+ path . join ( testDir , 'package.json' ) ,
701
+ JSON . stringify ( {
702
+ name : 'test-project' ,
703
+ version : '1.0.0' ,
704
+ dependencies : {
705
+ react : '17.0.2' ,
706
+ } ,
707
+ } , null , 2 ) ,
708
+ ) ;
709
+
710
+ // Create entry file
711
+ fs . writeFileSync (
712
+ path . join ( srcDir , 'index.js' ) ,
713
+ `
714
+ import React from 'react';
715
+ console.log('React version:', React.version);
716
+ ` ,
717
+ ) ;
718
+
719
+ const config : Configuration = {
720
+ mode : 'development' ,
721
+ context : testDir ,
722
+ entry : path . join ( srcDir , 'index.js' ) ,
723
+ output : {
724
+ path : path . join ( testDir , 'dist' ) ,
725
+ filename : 'bundle.js' ,
726
+ } ,
727
+ resolve : {
728
+ extensions : [ '.js' , '.json' ] ,
729
+ } ,
730
+ plugins : [
731
+ new FederationRuntimePlugin ( {
732
+ name : 'consumer' ,
733
+ filename : 'remoteEntry.js' ,
734
+ } ) ,
735
+ new ConsumeSharedPlugin ( {
736
+ consumes : {
737
+ react : {
738
+ import : 'react' ,
739
+ shareKey : 'react' ,
740
+ shareScope : 'default' ,
741
+ requiredVersion : '^17.0.0' ,
742
+ include : {
743
+ version : '^17.0.0' , // Should include React 17.x.x
744
+ } ,
745
+ singleton : false ,
746
+ } ,
747
+ } ,
748
+ } ) ,
749
+ ] ,
750
+ } ;
751
+
752
+ const compiler = webpack ( config ) ;
753
+ const stats = await compile ( compiler ) ;
754
+
755
+ expect ( stats . hasErrors ( ) ) . toBe ( false ) ;
756
+ expect ( stats . hasWarnings ( ) ) . toBe ( false ) ;
757
+
758
+ const output = stats . toJson ( ) ;
759
+ console . log ( output ) ;
760
+ const consumeSharedModules = output . modules ?. filter (
761
+ ( m ) =>
762
+ m . moduleType === 'consume-shared-module' &&
763
+ m . name ?. includes ( 'react' ) ,
764
+ ) ;
765
+ console . log ( consumeSharedModules ) ;
766
+ // Module should be included since version matches include pattern
767
+ expect ( consumeSharedModules ) . toBeDefined ( ) ;
768
+ expect ( consumeSharedModules ?. length ) . toBeGreaterThan ( 0 ) ;
769
+ // There should be at least one module
770
+ expect ( consumeSharedModules . length ) . toBeGreaterThan ( 0 ) ;
771
+ // All included modules should point to the correct fallback path ([email protected] )
772
+ consumeSharedModules . forEach ( module => {
773
+ expect ( module . identifier ) . toContain ( 'node_modules/react/index.js' ) ;
774
+ expect ( module . identifier ) . not . toContain ( '16.' ) ;
775
+ } ) ;
776
+ } ) ;
777
+
778
+ it ( 'should include only root module when version matches include.version' , async ( ) => {
779
+ // Setup React v17.0.2 in the root node_modules (should be included)
780
+ fs . writeFileSync (
781
+ path . join ( nodeModulesDir , 'react/package.json' ) ,
782
+ JSON . stringify ( {
783
+ name : 'react' ,
784
+ version : '17.0.2' ,
785
+ } ) ,
786
+ ) ;
787
+ fs . writeFileSync (
788
+ path . join ( nodeModulesDir , 'react/index.js' ) ,
789
+ 'module.exports = { version: "17.0.2" };' ,
790
+ ) ;
791
+
792
+ // Setup React v16.8.0 in a nested node_modules (should be excluded)
793
+ const nestedPackageDir = path . join ( nodeModulesDir , 'some-package' ) ;
794
+ const nestedReactDir = path . join ( nestedPackageDir , 'node_modules/react' ) ;
795
+ fs . mkdirSync ( nestedReactDir , { recursive : true } ) ;
796
+ fs . writeFileSync (
797
+ path . join ( nestedReactDir , 'package.json' ) ,
798
+ JSON . stringify ( {
799
+ name : 'react' ,
800
+ version : '16.8.0' ,
801
+ } ) ,
802
+ ) ;
803
+ fs . writeFileSync (
804
+ path . join ( nestedReactDir , 'index.js' ) ,
805
+ 'module.exports = { version: "16.8.0" };' ,
806
+ ) ;
807
+ fs . writeFileSync (
808
+ path . join ( nestedPackageDir , 'package.json' ) ,
809
+ JSON . stringify ( {
810
+ name : 'some-package' ,
811
+ version : '1.0.0' ,
812
+ dependencies : { react : '^16.0.0' } ,
813
+ } ) ,
814
+ ) ;
815
+ // Ensure some-package/index.js imports its own local react
816
+ fs . writeFileSync (
817
+ path . join ( nestedPackageDir , 'index.js' ) ,
818
+ 'import React from "react"; export default React;' ,
819
+ ) ;
820
+
821
+ // Create entry file that imports from both paths
822
+ fs . writeFileSync (
823
+ path . join ( srcDir , 'index.js' ) ,
824
+ `
825
+ import RootReact from "react";
826
+ import NestedReactPkg from "some-package";
827
+ console.log(RootReact.version, NestedReactPkg.default.version);
828
+ ` ,
829
+ ) ;
830
+
831
+ const config = {
832
+ mode : 'development' ,
833
+ context : testDir ,
834
+ entry : path . join ( srcDir , 'index.js' ) ,
835
+ output : {
836
+ path : path . join ( testDir , 'dist' ) ,
837
+ filename : 'bundle.js' ,
838
+ } ,
839
+ resolve : {
840
+ extensions : [ '.js' , '.json' ] ,
841
+ modules : [
842
+ 'node_modules' ,
843
+ path . join ( testDir , 'node_modules' ) ,
844
+ ] ,
845
+ } ,
846
+ plugins : [
847
+ new FederationRuntimePlugin ( {
848
+ name : 'consumer' ,
849
+ filename : 'remoteEntry.js' ,
850
+ } ) ,
851
+ new ConsumeSharedPlugin ( {
852
+ consumes : {
853
+ react : {
854
+ shareKey : 'react' ,
855
+ shareScope : 'default' ,
856
+ include : {
857
+ version : '^17.0.0' , // Should only include React 17.x.x
858
+ } ,
859
+ singleton : false ,
860
+ } ,
861
+ } ,
862
+ } ) ,
863
+ ] ,
864
+ } ;
865
+
866
+ const compiler = webpack ( config ) ;
867
+ const stats = await compile ( compiler ) ;
868
+
869
+ expect ( stats . hasErrors ( ) ) . toBe ( false ) ;
870
+ expect ( stats . hasWarnings ( ) ) . toBe ( false ) ;
871
+
872
+ const output = stats . toJson ( { modules : true } ) ;
873
+ const consumeSharedModules = output . modules ?. filter (
874
+ ( m ) => m . moduleType === 'consume-shared-module' && m . name ?. includes ( 'react' ) ,
875
+ ) || [ ] ;
876
+ // There should be at least one module for the correct version (root)
877
+ expect ( consumeSharedModules . some ( m => m . identifier . includes ( 'node_modules/react/index.js' ) && ! m . identifier . includes ( 'some-package' ) ) ) . toBe ( true ) ;
878
+ // There should be no modules for the nested version (16.x.x)
879
+ expect ( consumeSharedModules . some ( m => m . identifier . includes ( 'some-package/node_modules/react/index.js' ) ) ) . toBe ( false ) ;
880
+ } ) ;
881
+
882
+ it ( 'should include only nested module when version matches include.version (multi-version structure)' , async ( ) => {
883
+ // Setup [email protected] in the root node_modules (should be excluded)
884
+ const sharedRootDir = path . join ( nodeModulesDir , 'shared' ) ;
885
+ fs . mkdirSync ( sharedRootDir , { recursive : true } ) ;
886
+ fs . writeFileSync (
887
+ path . join ( sharedRootDir , 'package.json' ) ,
888
+ JSON . stringify ( { name : 'shared' , version : '1.0.0' } ) ,
889
+ ) ;
890
+ fs . writeFileSync (
891
+ path . join ( sharedRootDir , 'index.js' ) ,
892
+ 'module.exports = { version: "1.0.0" };' ,
893
+ ) ;
894
+
895
+ // Add a base package.json to testDir to avoid missing dependency warnings
896
+ fs . writeFileSync (
897
+ path . join ( testDir , 'package.json' ) ,
898
+ JSON . stringify ( {
899
+ name : 'test-multi-version-include' ,
900
+ version : '1.0.0' ,
901
+ dependencies : {
902
+ shared : '1.0.0' ,
903
+ 'my-module' : '1.0.0' ,
904
+ } ,
905
+ } , null , 2 ) ,
906
+ ) ;
907
+
908
+ // Setup my-module with its own node_modules/[email protected] (should be included)
909
+ const myModuleDir = path . join ( nodeModulesDir , 'my-module' ) ;
910
+ const myModuleNodeModules = path . join ( myModuleDir , 'node_modules' ) ;
911
+ const sharedNestedDir = path . join ( myModuleNodeModules , 'shared' ) ;
912
+ fs . mkdirSync ( sharedNestedDir , { recursive : true } ) ;
913
+ fs . writeFileSync (
914
+ path . join ( sharedNestedDir , 'package.json' ) ,
915
+ JSON . stringify ( { name : 'shared' , version : '2.0.0' } ) ,
916
+ ) ;
917
+ fs . writeFileSync (
918
+ path . join ( sharedNestedDir , 'index.js' ) ,
919
+ 'module.exports = { version: "2.0.0" };' ,
920
+ ) ;
921
+ fs . writeFileSync (
922
+ path . join ( myModuleDir , 'package.json' ) ,
923
+ JSON . stringify ( { name : 'my-module' , version : '1.0.0' , dependencies : { shared : '^2.0.0' } } ) ,
924
+ ) ;
925
+ fs . writeFileSync (
926
+ path . join ( myModuleDir , 'index.js' ) ,
927
+ 'import shared from "shared"; export const version = shared.version;' ,
928
+ ) ;
929
+
930
+ // Create entry file that imports from both paths
931
+ fs . writeFileSync (
932
+ path . join ( srcDir , 'index.js' ) ,
933
+ `
934
+ import shared from "shared";
935
+ import * as myModule from "my-module";
936
+ console.log(shared.version, myModule.version);
937
+ ` ,
938
+ ) ;
939
+
940
+ const config = {
941
+ mode : 'development' ,
942
+ context : testDir ,
943
+ entry : path . join ( srcDir , 'index.js' ) ,
944
+ output : {
945
+ path : path . join ( testDir , 'dist' ) ,
946
+ filename : 'bundle.js' ,
947
+ } ,
948
+ resolve : {
949
+ extensions : [ '.js' , '.json' ] ,
950
+ modules : [
951
+ 'node_modules' ,
952
+ path . join ( testDir , 'node_modules' ) ,
953
+ ] ,
954
+ } ,
955
+ plugins : [
956
+ new FederationRuntimePlugin ( {
957
+ name : 'consumer' ,
958
+ filename : 'remoteEntry.js' ,
959
+ } ) ,
960
+ new ConsumeSharedPlugin ( {
961
+ consumes : {
962
+ shared : {
963
+ shareKey : 'shared' ,
964
+ shareScope : 'default' ,
965
+ include : {
966
+ version :
'^2.0.0' , // Should only include [email protected]
967
+ } ,
968
+ singleton : false ,
969
+ } ,
970
+ } ,
971
+ } ) ,
972
+ ] ,
973
+ } ;
974
+
975
+ const compiler = webpack ( config ) ;
976
+ const stats = await compile ( compiler ) ;
977
+
978
+ expect ( stats . hasErrors ( ) ) . toBe ( false ) ;
979
+ expect ( stats . hasWarnings ( ) ) . toBe ( false ) ;
980
+
981
+ const output = stats . toJson ( { modules : true } ) ;
982
+ const consumeSharedModules = output . modules ?. filter (
983
+ ( m ) => m . moduleType === 'consume-shared-module' && m . name ?. includes ( 'shared' ) ,
984
+ ) || [ ] ;
985
+ // --- DEBUG LOGGING ---
986
+ console . log ( 'DEBUG: consumeSharedModules:' , consumeSharedModules . map ( m => ( { identifier : m . identifier , version : m . version } ) ) ) ;
987
+ // There should be at least one module for the correct version (nested)
988
+ expect ( consumeSharedModules . some ( m => m . identifier . includes ( 'my-module/node_modules/shared/index.js' ) ) ) . toBe ( true ) ;
989
+ // There should be no modules for the root version (1.0.0)
990
+ expect ( consumeSharedModules . some ( m => m . identifier . includes ( 'node_modules/shared/index.js' ) && ! m . identifier . includes ( 'my-module' ) ) ) . toBe ( false ) ;
991
+ } ) ;
992
+ } ) ;
993
+ } ) ;
651
994
} ) ;
0 commit comments