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.
### host
Type: `string`
The name of the host application that the remote application will be consumed by.
### inlineStyle
Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
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
Type: `number`

View File

@ -50,6 +50,12 @@ Possible values: `protractor`, `cypress`, `none`
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
Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
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
Type: `number`

View File

@ -50,6 +50,12 @@ Possible values: `protractor`, `cypress`, `none`
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
Alias(es): s

View File

@ -40,6 +40,12 @@ Possible values: `host`, `remote`
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
Type: `number`

View File

@ -71,6 +71,7 @@
"@nrwl/tao": "12.6.0-beta.2",
"@nrwl/web": "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",
"@popperjs/core": "^2.9.2",
"@reduxjs/toolkit": "1.5.0",
@ -280,4 +281,4 @@
"ng-packagr/rxjs": "6.6.7",
"**/xmlhttprequest-ssl": "~1.6.2"
}
}
}

View File

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

View File

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

View File

@ -1,5 +1,94 @@
// 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`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
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,
port: options.port,
remotes: options.remotes,
host: options.host,
skipFormat: true,
});
}

View File

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

View File

@ -141,6 +141,10 @@
"remotes": {
"type": "array",
"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": []

View File

@ -1,5 +1,94 @@
// 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`] = `
"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\");
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-remote-to-host';
export * from './change-build-target';
export * from './fix-bootstrap';
export * from './generate-config';

View File

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

View File

@ -29,6 +29,10 @@
"type": "array",
"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": {
"type": "boolean",
"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');
});
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 {
addImplicitDeps,
addRemoteToHost,
changeBuildTarget,
fixBootstrap,
generateWebpackConfig,
@ -20,6 +21,7 @@ export async function setupMfe(host: Tree, options: Schema) {
const projectConfig = readProjectConfiguration(host, options.appName);
const remotesWithPorts = getRemotesWithPorts(host, options);
addRemoteToHost(host, options);
generateWebpackConfig(host, options, projectConfig.root, remotesWithPorts);

View File

@ -4,7 +4,7 @@ import { satisfies } from 'semver';
// Ignore packages that are defined here per package
const IGNORE_MATCHES = {
'*': [],
angular: ['webpack-merge'],
angular: ['webpack-merge', '@phenomnomnominal/tsquery'],
};
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"
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":
version "0.4.3"
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"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.2.0:
esquery@^1.0.1, esquery@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==