Skip to content

Commit

Permalink
feat(angular): add support for passing additional shared dependencies…
Browse files Browse the repository at this point in the history
… in the module federation config (#10156)
  • Loading branch information
leosvelperez committed May 5, 2022
1 parent 0a1e822 commit b8c175f
Show file tree
Hide file tree
Showing 5 changed files with 410 additions and 57 deletions.
@@ -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
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
@@ -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
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.'
);
});
});

0 comments on commit b8c175f

Please sign in to comment.