feat(angular): add host option to MFE generator (#6368)

Add a host option to MFE generator to allow a remote to specify a host that it should be consumed
by.
Use this value to update the host application's webpack.config,js
This commit is contained in:
Colum Ferry 2021-07-15 09:54:41 +01:00 committed by GitHub
parent 904b3b6b7a
commit 776bd277b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 380 additions and 3 deletions

View File

@ -50,6 +50,12 @@ Possible values: `protractor`, `cypress`, `none`
Test runner to use for end to end (e2e) tests. Test runner to use for end to end (e2e) tests.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### inlineStyle ### inlineStyle
Alias(es): s Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for. Type of application to generate the Module Federation configuration for.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### port ### port
Type: `number` Type: `number`

View File

@ -50,6 +50,12 @@ Possible values: `protractor`, `cypress`, `none`
Test runner to use for end to end (e2e) tests. Test runner to use for end to end (e2e) tests.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### inlineStyle ### inlineStyle
Alias(es): s Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for. Type of application to generate the Module Federation configuration for.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### port ### port
Type: `number` Type: `number`

View File

@ -50,6 +50,12 @@ Possible values: `protractor`, `cypress`, `none`
Test runner to use for end to end (e2e) tests. Test runner to use for end to end (e2e) tests.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### inlineStyle ### inlineStyle
Alias(es): s Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for. Type of application to generate the Module Federation configuration for.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### port ### port
Type: `number` Type: `number`

View File

@ -71,6 +71,7 @@
"@nrwl/tao": "12.6.0-beta.2", "@nrwl/tao": "12.6.0-beta.2",
"@nrwl/web": "12.6.0-beta.2", "@nrwl/web": "12.6.0-beta.2",
"@nrwl/workspace": "12.6.0-beta.2", "@nrwl/workspace": "12.6.0-beta.2",
"@phenomnomnominal/tsquery": "4.1.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"@reduxjs/toolkit": "1.5.0", "@reduxjs/toolkit": "1.5.0",

View File

@ -15,6 +15,7 @@
"@angular-devkit", "@angular-devkit",
"@angular-eslint/", "@angular-eslint/",
"@schematics", "@schematics",
"@phenomnomnominal/tsquery",
"ignore", "ignore",
"jasmine-marbles", "jasmine-marbles",
"rxjs-for-await", "rxjs-for-await",

View File

@ -40,6 +40,7 @@
"@nrwl/linter": "*", "@nrwl/linter": "*",
"@nrwl/storybook": "*", "@nrwl/storybook": "*",
"@schematics/angular": "^12.0.0", "@schematics/angular": "^12.0.0",
"@phenomnomnominal/tsquery": "4.1.1",
"ignore": "^5.0.4", "ignore": "^5.0.4",
"jasmine-marbles": "~0.6.0", "jasmine-marbles": "~0.6.0",
"rxjs-for-await": "0.0.2", "rxjs-for-await": "0.0.2",

View File

@ -1,5 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app --mfe should add a remote application and add it to a specified host applications webpack config that contains a remote application already 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\");
const path = require(\\"path\\");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, \\"../../tsconfig.base.json\\"), [
/* mapped paths to share */
]);
module.exports = {
output: {
uniqueName: \\"app1\\",
publicPath: \\"auto\\",
},
optimization: {
runtimeChunk: false,
minimize: false,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
remotes: {
remote1: 'remote1@http://localhost:4201/remoteEntry.js',
remote2: 'remote2@http://localhost:4202/remoteEntry.js',
},
shared: {
\\"@angular/core\\": { singleton: true, strictVersion: true },
\\"@angular/common\\": { singleton: true, strictVersion: true },
\\"@angular/common/http\\": { singleton: true, strictVersion: true },
\\"@angular/router\\": { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
}),
sharedMappings.getPlugin(),
],
};
"
`;
exports[`app --mfe should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\");
const path = require(\\"path\\");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, \\"../../tsconfig.base.json\\"), [
/* mapped paths to share */
]);
module.exports = {
output: {
uniqueName: \\"app1\\",
publicPath: \\"auto\\",
},
optimization: {
runtimeChunk: false,
minimize: false,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
remotes: {
remote1: 'remote1@http://localhost:4200/remoteEntry.js',
},
shared: {
\\"@angular/core\\": { singleton: true, strictVersion: true },
\\"@angular/common\\": { singleton: true, strictVersion: true },
\\"@angular/common/http\\": { singleton: true, strictVersion: true },
\\"@angular/router\\": { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
}),
sharedMappings.getPlugin(),
],
};
"
`;
exports[`app --mfe should generate a Module Federation correctly for a each app 1`] = ` exports[`app --mfe should generate a Module Federation correctly for a each app 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\"); "const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\"); const mf = require(\\"@angular-architects/module-federation/webpack\\");

View File

@ -679,6 +679,58 @@ describe('app', () => {
); );
} }
); );
it('should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it', async () => {
// ARRANGE
await generateApp(appTree, 'app1', {
mfe: true,
mfeType: 'host',
});
// ACT
await generateApp(appTree, 'remote1', {
mfe: true,
mfeType: 'remote',
host: 'app1',
});
// ASSERT
const hostWebpackConfig = appTree.read(
'apps/app1/webpack.config.js',
'utf-8'
);
expect(hostWebpackConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications webpack config that contains a remote application already', async () => {
// ARRANGE
await generateApp(appTree, 'app1', {
mfe: true,
mfeType: 'host',
});
await generateApp(appTree, 'remote1', {
mfe: true,
mfeType: 'remote',
host: 'app1',
port: 4201,
});
// ACT
await generateApp(appTree, 'remote2', {
mfe: true,
mfeType: 'remote',
host: 'app1',
port: 4202,
});
// ASSERT
const hostWebpackConfig = appTree.read(
'apps/app1/webpack.config.js',
'utf-8'
);
expect(hostWebpackConfig).toMatchSnapshot();
});
}); });
}); });

View File

@ -9,6 +9,7 @@ export async function addMfe(host: Tree, options: NormalizedSchema) {
mfeType: options.mfeType, mfeType: options.mfeType,
port: options.port, port: options.port,
remotes: options.remotes, remotes: options.remotes,
host: options.host,
skipFormat: true, skipFormat: true,
}); });
} }

View File

@ -24,4 +24,5 @@ export interface Schema {
mfeType?: 'host' | 'remote'; mfeType?: 'host' | 'remote';
remotes?: string[]; remotes?: string[];
port?: number; port?: number;
host?: string;
} }

View File

@ -141,6 +141,10 @@
"remotes": { "remotes": {
"type": "array", "type": "array",
"description": "A list of remote application names that the host application should consume." "description": "A list of remote application names that the host application should consume."
},
"host": {
"type": "string",
"description": "The name of the host application that the remote application will be consumed by."
} }
}, },
"required": [] "required": []

View File

@ -1,5 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Init MFE should add a remote application and add it to a specified host applications webpack config that contains a remote application already 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\");
const path = require(\\"path\\");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, \\"../../tsconfig.base.json\\"), [
/* mapped paths to share */
]);
module.exports = {
output: {
uniqueName: \\"app1\\",
publicPath: \\"auto\\",
},
optimization: {
runtimeChunk: false,
minimize: false,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
remotes: {
remote1: 'remote1@http://localhost:4201/remoteEntry.js',
remote2: 'remote2@http://localhost:4202/remoteEntry.js',
},
shared: {
\\"@angular/core\\": { singleton: true, strictVersion: true },
\\"@angular/common\\": { singleton: true, strictVersion: true },
\\"@angular/common/http\\": { singleton: true, strictVersion: true },
\\"@angular/router\\": { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
}),
sharedMappings.getPlugin(),
],
};
"
`;
exports[`Init MFE should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\");
const path = require(\\"path\\");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, \\"../../tsconfig.base.json\\"), [
/* mapped paths to share */
]);
module.exports = {
output: {
uniqueName: \\"app1\\",
publicPath: \\"auto\\",
},
optimization: {
runtimeChunk: false,
minimize: false,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
remotes: {
remote1: 'remote1@http://localhost:4200/remoteEntry.js',
},
shared: {
\\"@angular/core\\": { singleton: true, strictVersion: true },
\\"@angular/common\\": { singleton: true, strictVersion: true },
\\"@angular/common/http\\": { singleton: true, strictVersion: true },
\\"@angular/router\\": { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
}),
sharedMappings.getPlugin(),
],
};
"
`;
exports[`Init MFE should create webpack configs correctly 1`] = ` exports[`Init MFE should create webpack configs correctly 1`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\"); "const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
const mf = require(\\"@angular-architects/module-federation/webpack\\"); const mf = require(\\"@angular-architects/module-federation/webpack\\");

View File

@ -0,0 +1,37 @@
import type { Tree } from '@nrwl/devkit';
import type { Schema } from '../schema';
import { readProjectConfiguration } from '@nrwl/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import { ObjectLiteralExpression } from 'typescript';
export function addRemoteToHost(host: Tree, options: Schema) {
if (options.mfeType === 'remote' && options.host) {
const project = readProjectConfiguration(host, options.host);
const hostWebpackPath =
project.targets['build'].options.customWebpackConfig?.path;
if (!hostWebpackPath || !host.exists(hostWebpackPath)) {
throw new Error(
`The selected host application, ${options.host}, does not contain a webpack.config.js. Are you sure it has been set up as a host application?`
);
}
const hostWebpackConfig = host.read(hostWebpackPath, 'utf-8');
const webpackAst = tsquery.ast(hostWebpackConfig);
const mfRemotesNode = tsquery(
webpackAst,
'Identifier[name=remotes] ~ ObjectLiteralExpression',
{ visitAllChildren: true }
)[0] as ObjectLiteralExpression;
const endOfPropertiesPos = mfRemotesNode.properties.end;
const updatedConfig = `${hostWebpackConfig.slice(0, endOfPropertiesPos)}
\t\t${options.appName}: '${options.appName}@http://localhost:${
options.port ?? 4200
}/remoteEntry.js',${hostWebpackConfig.slice(endOfPropertiesPos)}`;
host.write(hostWebpackPath, updatedConfig);
}
}

View File

@ -1,4 +1,5 @@
export * from './add-implicit-deps'; export * from './add-implicit-deps';
export * from './add-remote-to-host';
export * from './change-build-target'; export * from './change-build-target';
export * from './fix-bootstrap'; export * from './fix-bootstrap';
export * from './generate-config'; export * from './generate-config';

View File

@ -3,5 +3,6 @@ export interface Schema {
mfeType: 'host' | 'remote'; mfeType: 'host' | 'remote';
port?: number; port?: number;
remotes?: string[]; remotes?: string[];
host?: string;
skipFormat?: boolean; skipFormat?: boolean;
} }

View File

@ -29,6 +29,10 @@
"type": "array", "type": "array",
"description": "A list of remote application names that the host application should consume." "description": "A list of remote application names that the host application should consume."
}, },
"host": {
"type": "string",
"description": "The name of the host application that the remote application will be consumed by."
},
"skipFormat": { "skipFormat": {
"type": "boolean", "type": "boolean",
"description": "Skip formatting the workspace after the generator completes." "description": "Skip formatting the workspace after the generator completes."

View File

@ -164,4 +164,54 @@ describe('Init MFE', () => {
expect(nxJson.projects['app1'].implicitDependencies).toContain('remote1'); expect(nxJson.projects['app1'].implicitDependencies).toContain('remote1');
}); });
it('should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it', async () => {
// ARRANGE
await setupMfe(host, {
appName: 'app1',
mfeType: 'host',
});
// ACT
await setupMfe(host, {
appName: 'remote1',
mfeType: 'remote',
host: 'app1',
});
// ASSERT
const hostWebpackConfig = host.read('apps/app1/webpack.config.js', 'utf-8');
expect(hostWebpackConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications webpack config that contains a remote application already', async () => {
// ARRANGE
await applicationGenerator(host, {
name: 'remote2',
});
await setupMfe(host, {
appName: 'app1',
mfeType: 'host',
});
await setupMfe(host, {
appName: 'remote1',
mfeType: 'remote',
host: 'app1',
port: 4201,
});
// ACT
await setupMfe(host, {
appName: 'remote2',
mfeType: 'remote',
host: 'app1',
port: 4202,
});
// ASSERT
const hostWebpackConfig = host.read('apps/app1/webpack.config.js', 'utf-8');
expect(hostWebpackConfig).toMatchSnapshot();
});
}); });

View File

@ -9,6 +9,7 @@ import {
import { import {
addImplicitDeps, addImplicitDeps,
addRemoteToHost,
changeBuildTarget, changeBuildTarget,
fixBootstrap, fixBootstrap,
generateWebpackConfig, generateWebpackConfig,
@ -20,6 +21,7 @@ export async function setupMfe(host: Tree, options: Schema) {
const projectConfig = readProjectConfiguration(host, options.appName); const projectConfig = readProjectConfiguration(host, options.appName);
const remotesWithPorts = getRemotesWithPorts(host, options); const remotesWithPorts = getRemotesWithPorts(host, options);
addRemoteToHost(host, options);
generateWebpackConfig(host, options, projectConfig.root, remotesWithPorts); generateWebpackConfig(host, options, projectConfig.root, remotesWithPorts);

View File

@ -4,7 +4,7 @@ import { satisfies } from 'semver';
// Ignore packages that are defined here per package // Ignore packages that are defined here per package
const IGNORE_MATCHES = { const IGNORE_MATCHES = {
'*': [], '*': [],
angular: ['webpack-merge'], angular: ['webpack-merge', '@phenomnomnominal/tsquery'],
}; };
export default function getDiscrepancies( export default function getDiscrepancies(

View File

@ -3765,6 +3765,13 @@
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001"
integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw==
"@phenomnomnominal/tsquery@4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz#42971b83590e9d853d024ddb04a18085a36518df"
integrity sha512-jjMmK1tnZbm1Jq5a7fBliM4gQwjxMU7TFoRNwIyzwlO+eHPRCFv/Nv+H/Gi1jc3WR7QURG8D5d0Tn12YGrUqBQ==
dependencies:
esquery "^1.0.1"
"@pmmmwh/react-refresh-webpack-plugin@^0.4.3": "@pmmmwh/react-refresh-webpack-plugin@^0.4.3":
version "0.4.3" version "0.4.3"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766"
@ -11700,7 +11707,7 @@ esprima@^4.0.0, esprima@^4.0.1:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.2.0: esquery@^1.0.1, esquery@^1.2.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==