diff --git a/e2e/react-core/src/react-module-federation.test.ts b/e2e/react-core/src/react-module-federation.test.ts
index f03f99fa2c513..600dd5473edd7 100644
--- a/e2e/react-core/src/react-module-federation.test.ts
+++ b/e2e/react-core/src/react-module-federation.test.ts
@@ -2,12 +2,16 @@ import { stripIndents } from '@nx/devkit';
import {
checkFilesExist,
cleanupProject,
+ killProcessAndPorts,
newProject,
readJson,
runCLI,
runCLIAsync,
+ runCommandUntil,
+ runE2ETests,
uniq,
updateFile,
+ updateJson,
} from '@nx/e2e/utils';
import { join } from 'path';
@@ -139,6 +143,145 @@ describe('React Module Federation', () => {
expect(buildOutput).toContain('Successfully ran target build');
}, 500_000);
+ it('should support different versions workspace libs for host and remote', async () => {
+ const shell = uniq('shell');
+ const remote = uniq('remote');
+ const lib = uniq('lib');
+
+ runCLI(
+ `generate @nx/react:host ${shell} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
+ );
+
+ runCLI(
+ `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided`
+ );
+
+ updateFile(
+ `${lib}/src/lib/${lib}.ts`,
+ stripIndents`
+ export const version = '0.0.1';
+ `
+ );
+
+ updateJson(`${lib}/package.json`, (json) => {
+ return {
+ ...json,
+ version: '0.0.1',
+ };
+ });
+
+ // Update host to use the lib
+ updateFile(
+ `${shell}/src/app/app.tsx`,
+ `
+ import * as React from 'react';
+
+ import NxWelcome from './nx-welcome';
+ import { version } from '@acme/${lib}';
+ import { Link, Route, Routes } from 'react-router-dom';
+
+ const About = React.lazy(() => import('${remote}/Module'));
+
+ export function App() {
+ return (
+
+
+ Lib version: { version }
+
+
+ -
+ Home
+
+
+ -
+ About
+
+
+
+ } />
+
+ } />
+
+
+ );
+ }
+
+ export default App;`
+ );
+
+ // Update remote to use the lib
+ updateFile(
+ `${remote}/src/app/app.tsx`,
+ `// eslint-disable-next-line @typescript-eslint/no-unused-vars
+
+ import styles from './app.module.css';
+ import { version } from '@acme/${lib}';
+
+ import NxWelcome from './nx-welcome';
+
+ export function App() {
+ return (
+
+
+ Lib version: { version }
+
+
+ );
+ }
+
+ export default App;`
+ );
+
+ // update remote e2e test to check the version
+ updateFile(
+ `${remote}-e2e/src/e2e/app.cy.ts`,
+ `describe('${remote}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should check the lib version', () => {
+ cy.get('div.remote').contains('Lib version: 0.0.1');
+ });
+ });
+ `
+ );
+
+ // update shell e2e test to check the version
+ updateFile(
+ `${shell}-e2e/src/e2e/app.cy.ts`,
+ `
+ describe('${shell}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should check the lib version', () => {
+ cy.get('div.home').contains('Lib version: 0.0.1');
+ });
+ });
+ `
+ );
+
+ if (runE2ETests()) {
+ // test remote e2e
+ const remoteE2eResults = runCLI(`e2e ${remote}-e2e --no-watch --verbose`);
+ expect(remoteE2eResults).toContain('All specs passed!');
+
+ // test shell e2e
+ // serve remote first
+ const remotePort = 4201;
+ const remoteProcess = await runCommandUntil(
+ `serve ${remote} --no-watch --verbose`,
+ (output) => {
+ return output.includes(
+ `Web Development Server is listening at http://localhost:${remotePort}/`
+ );
+ }
+ );
+ const shellE2eResults = runCLI(`e2e ${shell}-e2e --no-watch --verbose`);
+ expect(shellE2eResults).toContain('All specs passed!');
+
+ await killProcessAndPorts(remoteProcess.pid, remotePort);
+ }
+ }, 500_000);
+
function readPort(appName: string): number {
const config = readJson(join('apps', appName, 'project.json'));
return config.targets.serve.options.port;
diff --git a/packages/angular/src/utils/mf/utils.ts b/packages/angular/src/utils/mf/utils.ts
index 101142ad5796e..b63c5e061dec2 100644
--- a/packages/angular/src/utils/mf/utils.ts
+++ b/packages/angular/src/utils/mf/utils.ts
@@ -124,7 +124,9 @@ export async function getModuleFederationConfig(
});
const sharedDependencies = {
- ...sharedLibraries.getLibraries(),
+ ...sharedLibraries.getLibraries(
+ projectGraph.nodes[mfConfig.name].data.root
+ ),
...npmPackages,
};
diff --git a/packages/react/src/module-federation/utils.ts b/packages/react/src/module-federation/utils.ts
index ce46bb2f8a05a..7775123b1dce6 100644
--- a/packages/react/src/module-federation/utils.ts
+++ b/packages/react/src/module-federation/utils.ts
@@ -84,7 +84,7 @@ export async function getModuleFederationConfig(
const npmPackages = sharePackages(dependencies.npmPackages);
const sharedDependencies = {
- ...sharedLibraries.getLibraries(),
+ ...sharedLibraries.getLibraries(project.root),
...npmPackages,
};
diff --git a/packages/webpack/src/utils/module-federation/models/index.ts b/packages/webpack/src/utils/module-federation/models/index.ts
index 5d0365c213988..36653056fbffc 100644
--- a/packages/webpack/src/utils/module-federation/models/index.ts
+++ b/packages/webpack/src/utils/module-federation/models/index.ts
@@ -1,6 +1,7 @@
import type { NormalModuleReplacementPlugin } from 'webpack';
export type ModuleFederationLibrary = { type: string; name: string };
+
export type WorkspaceLibrary = {
name: string;
root: string;
@@ -9,7 +10,10 @@ export type WorkspaceLibrary = {
export type SharedWorkspaceLibraryConfig = {
getAliases: () => Record;
- getLibraries: (eager?: boolean) => Record;
+ getLibraries: (
+ projectRoot: string,
+ eager?: boolean
+ ) => Record;
getReplacementPlugin: () => NormalModuleReplacementPlugin;
};
diff --git a/packages/webpack/src/utils/module-federation/share.spec.ts b/packages/webpack/src/utils/module-federation/share.spec.ts
index d7cb824f211ed..21cd14e61b73f 100644
--- a/packages/webpack/src/utils/module-federation/share.spec.ts
+++ b/packages/webpack/src/utils/module-federation/share.spec.ts
@@ -50,7 +50,7 @@ describe('MF Share Utils', () => {
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
- expect(sharedLibraries.getLibraries()).toEqual({
+ expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
@@ -60,9 +60,7 @@ describe('MF Share Utils', () => {
it('should handle path mappings with wildcards correctly in non-buildable libraries', () => {
// ARRANGE
- jest
- .spyOn(fs, 'existsSync')
- .mockImplementation((file: string) => !file?.endsWith('package.json'));
+ jest.spyOn(fs, 'existsSync').mockImplementation((file: string) => true);
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/shared': ['/libs/shared/src/index.ts'],
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
@@ -78,7 +76,7 @@ describe('MF Share Utils', () => {
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
'libs/shared/src/index.ts'
);
- expect(sharedLibraries.getLibraries()).toEqual({
+ expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
'@myorg/shared': {
eager: undefined,
requiredVersion: false,
@@ -98,7 +96,7 @@ describe('MF Share Utils', () => {
// ASSERT
expect(sharedLibraries.getAliases()).toEqual({});
- expect(sharedLibraries.getLibraries()).toEqual({});
+ expect(sharedLibraries.getLibraries('libs/shared')).toEqual({});
});
});
@@ -371,6 +369,78 @@ describe('MF Share Utils', () => {
).not.toThrow();
});
});
+
+ it('should using shared library version from root package.json if available', () => {
+ // ARRANGE
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
+ jest
+ .spyOn(nxFileutils, 'readJsonFile')
+ .mockImplementation((file: string) => {
+ if (file.endsWith('package.json')) {
+ return {
+ dependencies: {
+ '@myorg/shared': '1.0.0',
+ },
+ };
+ }
+ });
+
+ jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
+ '@myorg/shared': ['/libs/shared/src/index.ts'],
+ '@myorg/shared/*': ['/libs/shared/src/lib/*'],
+ });
+
+ // ACT
+ const sharedLibraries = shareWorkspaceLibraries(
+ [{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
+ '/'
+ );
+
+ // ASSERT
+ expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
+ '@myorg/shared': {
+ eager: undefined,
+ requiredVersion: '1.0.0',
+ singleton: true,
+ },
+ });
+ });
+
+ it('should use shared library version from library package.json if project package.json does not have it', () => {
+ // ARRANGE
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
+ jest
+ .spyOn(nxFileutils, 'readJsonFile')
+ .mockImplementation((file: string) => {
+ if (file.endsWith('libs/shared/package.json')) {
+ return {
+ version: '1.0.0',
+ };
+ } else {
+ return {};
+ }
+ });
+
+ jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
+ '@myorg/shared': ['/libs/shared/src/index.ts'],
+ '@myorg/shared/*': ['/libs/shared/src/lib/*'],
+ });
+
+ // ACT
+ const sharedLibraries = shareWorkspaceLibraries(
+ [{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' }],
+ null
+ );
+
+ // ASSERT
+ expect(sharedLibraries.getLibraries('libs/shared')).toEqual({
+ '@myorg/shared': {
+ eager: undefined,
+ requiredVersion: '1.0.0',
+ singleton: true,
+ },
+ });
+ });
});
function createMockedFSForNestedEntryPoints() {
diff --git a/packages/webpack/src/utils/module-federation/share.ts b/packages/webpack/src/utils/module-federation/share.ts
index ceeba7cc55238..b0188c909564d 100644
--- a/packages/webpack/src/utils/module-federation/share.ts
+++ b/packages/webpack/src/utils/module-federation/share.ts
@@ -11,20 +11,29 @@ import {
collectPackageSecondaryEntryPoints,
collectWorkspaceLibrarySecondaryEntryPoints,
} from './secondary-entry-points';
-import { type ProjectGraph, workspaceRoot, logger } from '@nx/devkit';
+import {
+ type ProjectGraph,
+ workspaceRoot,
+ logger,
+ readJsonFile,
+ ProjectGraphProjectNode,
+ joinPathFragments,
+} from '@nx/devkit';
+import { existsSync } from 'fs';
+import type { PackageJson } from 'nx/src/utils/package-json';
/**
* Build an object of functions to be used with the ModuleFederationPlugin to
* share Nx Workspace Libraries between Hosts and Remotes.
*
- * @param libraries - The Nx Workspace Libraries to share
+ * @param workspaceLibs - The Nx Workspace Libraries to share
* @param tsConfigPath - The path to TS Config File that contains the Path Mappings for the Libraries
*/
export function shareWorkspaceLibraries(
- libraries: WorkspaceLibrary[],
+ workspaceLibs: WorkspaceLibrary[],
tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
): SharedWorkspaceLibraryConfig {
- if (!libraries) {
+ if (!workspaceLibs) {
return getEmptySharedLibrariesConfig();
}
@@ -35,7 +44,7 @@ export function shareWorkspaceLibraries(
const pathMappings: { name: string; path: string }[] = [];
for (const [key, paths] of Object.entries(tsconfigPathAliases)) {
- const library = libraries.find((lib) => lib.importKey === key);
+ const library = workspaceLibs.find((lib) => lib.importKey === key);
if (!library) {
continue;
}
@@ -66,14 +75,55 @@ export function shareWorkspaceLibraries(
(aliases, library) => ({ ...aliases, [library.name]: library.path }),
{}
),
- getLibraries: (eager?: boolean): Record =>
- pathMappings.reduce(
- (libraries, library) => ({
+ getLibraries: (
+ projectRoot: string,
+ eager?: boolean
+ ): Record => {
+ let pkgJson: PackageJson = null;
+ if (
+ projectRoot &&
+ existsSync(
+ joinPathFragments(workspaceRoot, projectRoot, 'package.json')
+ )
+ ) {
+ pkgJson = readJsonFile(
+ joinPathFragments(workspaceRoot, projectRoot, 'package.json')
+ );
+ }
+ return pathMappings.reduce((libraries, library) => {
+ // Check to see if the library version is declared in the app's package.json
+ let version = pkgJson?.dependencies?.[library.name];
+ if (!version && workspaceLibs.length > 0) {
+ const workspaceLib = workspaceLibs.find(
+ (lib) => lib.importKey === library.name
+ );
+
+ const libPackageJsonPath = workspaceLib
+ ? join(workspaceLib.root, 'package.json')
+ : null;
+ if (libPackageJsonPath && existsSync(libPackageJsonPath)) {
+ pkgJson = readJsonFile(libPackageJsonPath);
+
+ if (pkgJson) {
+ version = pkgJson.version;
+ }
+ }
+ }
+
+ return {
...libraries,
- [library.name]: { requiredVersion: false, eager },
- }),
- {} as Record
- ),
+ [library.name]: {
+ ...(version
+ ? {
+ requiredVersion: version,
+ singleton: true,
+ }
+ : { requiredVersion: false }),
+ eager,
+ },
+ };
+ }, {} as Record);
+ },
getReplacementPlugin: () =>
new webpack.NormalModuleReplacementPlugin(/./, (req) => {
if (!req.request.startsWith('.')) {