Skip to content

Commit

Permalink
feat(react): Add versioning for workspace libraries (#19063)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham authored Sep 14, 2023
1 parent de2824a commit 0a7efc6
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 21 deletions.
143 changes: 143 additions & 0 deletions e2e/react-core/src/react-module-federation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
<React.Suspense fallback={null}>
<div className="home">
Lib version: { version }
</div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/About">About</Link>
</li>
</ul>
<Routes>
<Route path="/" element={<NxWelcome title="home" />} />
<Route path="/About" element={<About />} />
</Routes>
</React.Suspense>
);
}
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 (
<div className='remote'>
Lib version: { version }
<NxWelcome title="${remote}" />
</div>
);
}
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;
Expand Down
4 changes: 3 additions & 1 deletion packages/angular/src/utils/mf/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export async function getModuleFederationConfig(
});

const sharedDependencies = {
...sharedLibraries.getLibraries(),
...sharedLibraries.getLibraries(
projectGraph.nodes[mfConfig.name].data.root
),
...npmPackages,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/module-federation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export async function getModuleFederationConfig(
const npmPackages = sharePackages(dependencies.npmPackages);

const sharedDependencies = {
...sharedLibraries.getLibraries(),
...sharedLibraries.getLibraries(project.root),
...npmPackages,
};

Expand Down
6 changes: 5 additions & 1 deletion packages/webpack/src/utils/module-federation/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NormalModuleReplacementPlugin } from 'webpack';

export type ModuleFederationLibrary = { type: string; name: string };

export type WorkspaceLibrary = {
name: string;
root: string;
Expand All @@ -9,7 +10,10 @@ export type WorkspaceLibrary = {

export type SharedWorkspaceLibraryConfig = {
getAliases: () => Record<string, string>;
getLibraries: (eager?: boolean) => Record<string, SharedLibraryConfig>;
getLibraries: (
projectRoot: string,
eager?: boolean
) => Record<string, SharedLibraryConfig>;
getReplacementPlugin: () => NormalModuleReplacementPlugin;
};

Expand Down
82 changes: 76 additions & 6 deletions packages/webpack/src/utils/module-federation/share.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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/*'],
Expand All @@ -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,
Expand All @@ -98,7 +96,7 @@ describe('MF Share Utils', () => {

// ASSERT
expect(sharedLibraries.getAliases()).toEqual({});
expect(sharedLibraries.getLibraries()).toEqual({});
expect(sharedLibraries.getLibraries('libs/shared')).toEqual({});
});
});

Expand Down Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 0a7efc6

Please sign in to comment.