Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular): add support for passing additional shared dependencies in the module federation config #10156

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withModuleFederation should apply additional shared dependencies when specified 1`] = `
Array [
ModuleFederationPlugin {
"_options": Object {
"exposes": Object {
"./Module": "apps/remote1/src/module.ts",
},
"filename": "remoteEntry.mjs",
"library": Object {
"type": "module",
},
"name": "remote1",
"remotes": Object {},
"shared": Object {
"@angular/common": Object {
"requiredVersion": "~13.2.0",
"singleton": true,
"strictVersion": true,
},
"@angular/core": Object {
"singleton": true,
"strictVersion": false,
},
"@angular/router": Object {
"requiredVersion": "^13.0.0",
"singleton": false,
"strictVersion": true,
},
"shared": Object {
"requiredVersion": false,
},
},
},
},
NormalModuleReplacementPlugin {
"newResource": [Function],
"resourceRegExp": /\\./,
},
]
`;

exports[`withModuleFederation should apply the user-specified shared function correctly 1`] = `
Array [
ModuleFederationPlugin {
"_options": Object {
"exposes": Object {
"./Module": "apps/remote1/src/module.ts",
},
"filename": "remoteEntry.mjs",
"library": Object {
"type": "module",
},
"name": "remote1",
"remotes": Object {},
"shared": Object {
"@angular/core": Object {
"requiredVersion": undefined,
"singleton": true,
"strictVersion": false,
},
"shared": Object {
"eager": undefined,
"requiredVersion": false,
},
},
},
},
NormalModuleReplacementPlugin {
"newResource": [Function],
"resourceRegExp": /\\./,
},
]
`;

exports[`withModuleFederation should collect dependencies correctly 1`] = `
Array [
ModuleFederationPlugin {
Expand Down
61 changes: 30 additions & 31 deletions packages/angular/src/utils/mfe/mfe-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import { existsSync, lstatSync, readdirSync } from 'fs';
import { dirname, join, normalize, relative } from 'path';
import { ParsedCommandLine } from 'typescript';
import { NormalModuleReplacementPlugin } from 'webpack';
import { readRootPackageJson } from './utils';

export interface SharedLibraryConfig {
singleton: boolean;
strictVersion: boolean;
requiredVersion: string;
eager: boolean;
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: false | string;
eager?: boolean;
}

export function shareWorkspaceLibraries(
Expand All @@ -36,7 +37,7 @@ export function shareWorkspaceLibraries(
if (!tsconfigPathAliases) {
return {
getAliases: () => [],
getLibraries: () => {},
getLibraries: () => ({}),
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, () => {}),
};
Expand Down Expand Up @@ -65,7 +66,7 @@ export function shareWorkspaceLibraries(
...libraries,
[library.name]: { requiredVersion: false, eager },
}),
{}
{} as Record<string, SharedLibraryConfig>
),
getReplacementPlugin: () =>
new NormalModuleReplacementPlugin(/./, (req) => {
Expand Down Expand Up @@ -150,17 +151,27 @@ function collectPackageSecondaryEntryPoints(
);
}

export function sharePackages(
packages: string[]
): Record<string, SharedLibraryConfig> {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MFE: Could not find root package.json to determine dependency versions.'
export function getNpmPackageSharedConfig(
pkgName: string,
version: string
): SharedLibraryConfig | undefined {
if (!version) {
logger.warn(
`Could not find a version for "${pkgName}" in the root "package.json" ` +
'when collecting shared packages for the Module Federation setup. ' +
'The package will not be shared.'
);

return undefined;
}

const pkgJson = readJsonFile(pkgJsonPath);
return { singleton: true, strictVersion: true, requiredVersion: version };
}

export function sharePackages(
packages: string[]
): Record<string, SharedLibraryConfig> {
const pkgJson = readRootPackageJson();
const allPackages: { name: string; version: string }[] = [];
packages.forEach((pkg) => {
const pkgVersion =
Expand All @@ -170,23 +181,11 @@ export function sharePackages(
});

return allPackages.reduce((shared, pkg) => {
if (!pkg.version) {
logger.warn(
`Could not find a version for "${pkg.name}" in the root "package.json" ` +
'when collecting shared packages for the Module Federation setup. ' +
'The package will not be shared.'
);

return shared;
const config = getNpmPackageSharedConfig(pkg.name, pkg.version);
if (config) {
shared[pkg.name] = config;
}

return {
...shared,
[pkg.name]: {
singleton: true,
strictVersion: true,
requiredVersion: pkg.version,
},
};
}, {});
return shared;
}, {} as Record<string, SharedLibraryConfig>);
}
16 changes: 16 additions & 0 deletions packages/angular/src/utils/mfe/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nrwl/devkit';
import { existsSync } from 'fs';

export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MFE: Could not find root package.json to determine dependency versions.'
);
}

return readJsonFile(pkgJsonPath);
}
186 changes: 186 additions & 0 deletions packages/angular/src/utils/mfe/with-module-federation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,190 @@ describe('withModuleFederation', () => {
// ASSERT
expect(config.plugins).toMatchSnapshot();
});

it('should apply the user-specified shared function correctly', async () => {
// ARRANGE
(graph.readCachedProjectGraph as jest.Mock).mockReturnValue({
dependencies: {
remote1: [
{ target: 'npm:@angular/core' },
{ target: 'npm:zone.js' },
{ target: 'core' },
],
core: [{ target: 'shared' }, { target: 'npm:@angular/core' }],
},
});
(fs.existsSync as jest.Mock).mockReturnValue(true);
jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({
dependencies: { '@angular/core': '~13.2.0' },
}));
(typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({
options: {
paths: {
shared: ['/libs/shared/src/index.ts'],
core: ['/libs/core/src/index.ts'],
},
},
});
(graph.Workspaces as jest.Mock).mockReturnValue({
readWorkspaceConfiguration: () => ({
projects: {
shared: {
sourceRoot: '/libs/shared/src/',
},
core: {
sourceRoot: '/libs/core/src/',
},
},
}),
});
(fs.readdirSync as jest.Mock).mockReturnValue([]);

// ACT
const config = (
await withModuleFederation({
name: 'remote1',
exposes: { './Module': 'apps/remote1/src/module.ts' },
shared: (dep, config) => {
if (dep === 'core') {
return false;
}
if (dep === '@angular/core') {
return {
...config,
strictVersion: false,
requiredVersion: undefined,
};
}
},
})
)({});

// ASSERT
expect(config.plugins).toMatchSnapshot();
});

it('should apply additional shared dependencies when specified', async () => {
// ARRANGE
(graph.readCachedProjectGraph as jest.Mock).mockReturnValue({
dependencies: {
remote1: [{ target: 'npm:@angular/core' }, { target: 'core' }],
core: [{ target: 'npm:@angular/core' }],
},
nodes: { shared: {} },
externalNodes: { '@angular/common': {} },
});
(fs.existsSync as jest.Mock).mockReturnValue(true);
jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({
dependencies: {
'@angular/core': '~13.2.0',
'@angular/common': '~13.2.0',
'@angular/router': '~13.2.0',
},
}));
(typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({
options: {
paths: {
shared: ['/libs/shared/src/index.ts'],
core: ['/libs/core/src/index.ts'],
},
},
});
(graph.Workspaces as jest.Mock).mockReturnValue({
readWorkspaceConfiguration: () => ({
projects: {
shared: {
sourceRoot: '/libs/shared/src/',
},
core: {
sourceRoot: '/libs/core/src/',
},
},
}),
});
(fs.readdirSync as jest.Mock).mockReturnValue([]);

// ACT
const config = (
await withModuleFederation({
name: 'remote1',
exposes: { './Module': 'apps/remote1/src/module.ts' },
shared: (dep, config) => {
if (dep === 'core') {
return false;
}
},
additionalShared: [
'@angular/common',
[
'@angular/core',
{
strictVersion: false,
singleton: true,
},
],
{
libraryName: '@angular/router',
sharedConfig: {
requiredVersion: '^13.0.0',
singleton: false,
strictVersion: true,
},
},
'shared',
],
})
)({});

// ASSERT
expect(config.plugins).toMatchSnapshot();
});

it('should throw when an additionalShared entry is a string and it is not in the project graph', async () => {
// ARRANGE
(graph.readCachedProjectGraph as jest.Mock).mockReturnValue({
dependencies: {
remote1: [{ target: 'npm:@angular/core' }, { target: 'core' }],
core: [{ target: 'npm:@angular/core' }],
},
nodes: {},
externalNodes: {},
});
(fs.existsSync as jest.Mock).mockReturnValue(true);
jest.spyOn(devkit, 'readJsonFile').mockImplementation(() => ({
dependencies: { '@angular/core': '~13.2.0' },
}));
(typescriptUtils.readTsConfig as jest.Mock).mockReturnValue({
options: {
paths: {
shared: ['/libs/shared/src/index.ts'],
core: ['/libs/core/src/index.ts'],
},
},
});
(graph.Workspaces as jest.Mock).mockReturnValue({
readWorkspaceConfiguration: () => ({
projects: {
shared: {
sourceRoot: '/libs/shared/src/',
},
core: {
sourceRoot: '/libs/core/src/',
},
},
}),
});
(fs.readdirSync as jest.Mock).mockReturnValue([]);

// ACT & ASSERT
await expect(
withModuleFederation({
name: 'remote1',
exposes: { './Module': 'apps/remote1/src/module.ts' },
additionalShared: ['shared'],
})
).rejects.toThrow(
'The specified dependency "shared" in the additionalShared configuration does not exist in the project graph.'
);
});
});