diff --git a/docs/angular/api-angular/generators/application.md b/docs/angular/api-angular/generators/application.md index 3472c3fa2c..5226a45dc2 100644 --- a/docs/angular/api-angular/generators/application.md +++ b/docs/angular/api-angular/generators/application.md @@ -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 diff --git a/docs/angular/api-angular/generators/setup-mfe.md b/docs/angular/api-angular/generators/setup-mfe.md index a0ab9a6f05..b18b9d364d 100644 --- a/docs/angular/api-angular/generators/setup-mfe.md +++ b/docs/angular/api-angular/generators/setup-mfe.md @@ -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` diff --git a/docs/node/api-angular/generators/application.md b/docs/node/api-angular/generators/application.md index 00a89ebd00..c0f8e1f7d9 100644 --- a/docs/node/api-angular/generators/application.md +++ b/docs/node/api-angular/generators/application.md @@ -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 diff --git a/docs/node/api-angular/generators/setup-mfe.md b/docs/node/api-angular/generators/setup-mfe.md index 1efa6be52e..b05618ad2c 100644 --- a/docs/node/api-angular/generators/setup-mfe.md +++ b/docs/node/api-angular/generators/setup-mfe.md @@ -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` diff --git a/docs/react/api-angular/generators/application.md b/docs/react/api-angular/generators/application.md index 00a89ebd00..c0f8e1f7d9 100644 --- a/docs/react/api-angular/generators/application.md +++ b/docs/react/api-angular/generators/application.md @@ -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 diff --git a/docs/react/api-angular/generators/setup-mfe.md b/docs/react/api-angular/generators/setup-mfe.md index 1efa6be52e..b05618ad2c 100644 --- a/docs/react/api-angular/generators/setup-mfe.md +++ b/docs/react/api-angular/generators/setup-mfe.md @@ -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` diff --git a/package.json b/package.json index dfa181696f..4d86c04698 100644 --- a/package.json +++ b/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 6983ee0be1..3cea93e6b9 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -15,6 +15,7 @@ "@angular-devkit", "@angular-eslint/", "@schematics", + "@phenomnomnominal/tsquery", "ignore", "jasmine-marbles", "rxjs-for-await", diff --git a/packages/angular/package.json b/packages/angular/package.json index bb3eee7604..b39df23c2c 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -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", diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index 1834421c84..9236a6e8d1 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -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\\"); diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index 48a7b609f0..b72ad15448 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -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(); + }); }); }); diff --git a/packages/angular/src/generators/application/lib/add-mfe.ts b/packages/angular/src/generators/application/lib/add-mfe.ts index a4ee4317dc..10ab404065 100644 --- a/packages/angular/src/generators/application/lib/add-mfe.ts +++ b/packages/angular/src/generators/application/lib/add-mfe.ts @@ -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, }); } diff --git a/packages/angular/src/generators/application/schema.d.ts b/packages/angular/src/generators/application/schema.d.ts index ddcd29ba7d..38b0a3579c 100644 --- a/packages/angular/src/generators/application/schema.d.ts +++ b/packages/angular/src/generators/application/schema.d.ts @@ -24,4 +24,5 @@ export interface Schema { mfeType?: 'host' | 'remote'; remotes?: string[]; port?: number; + host?: string; } diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index 7120c535ad..9119cff67e 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -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": [] diff --git a/packages/angular/src/generators/setup-mfe/__snapshots__/setup-mfe.spec.ts.snap b/packages/angular/src/generators/setup-mfe/__snapshots__/setup-mfe.spec.ts.snap index 6e45681f51..3b8afff903 100644 --- a/packages/angular/src/generators/setup-mfe/__snapshots__/setup-mfe.spec.ts.snap +++ b/packages/angular/src/generators/setup-mfe/__snapshots__/setup-mfe.spec.ts.snap @@ -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\\"); diff --git a/packages/angular/src/generators/setup-mfe/lib/add-remote-to-host.ts b/packages/angular/src/generators/setup-mfe/lib/add-remote-to-host.ts new file mode 100644 index 0000000000..880d44c269 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/add-remote-to-host.ts @@ -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); + } +} diff --git a/packages/angular/src/generators/setup-mfe/lib/index.ts b/packages/angular/src/generators/setup-mfe/lib/index.ts index b9a5b6adb7..0a87a4d1fd 100644 --- a/packages/angular/src/generators/setup-mfe/lib/index.ts +++ b/packages/angular/src/generators/setup-mfe/lib/index.ts @@ -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'; diff --git a/packages/angular/src/generators/setup-mfe/schema.d.ts b/packages/angular/src/generators/setup-mfe/schema.d.ts index f02f79da6e..6d941cbf6e 100644 --- a/packages/angular/src/generators/setup-mfe/schema.d.ts +++ b/packages/angular/src/generators/setup-mfe/schema.d.ts @@ -3,5 +3,6 @@ export interface Schema { mfeType: 'host' | 'remote'; port?: number; remotes?: string[]; + host?: string; skipFormat?: boolean; } diff --git a/packages/angular/src/generators/setup-mfe/schema.json b/packages/angular/src/generators/setup-mfe/schema.json index 432689135d..9ce265f963 100644 --- a/packages/angular/src/generators/setup-mfe/schema.json +++ b/packages/angular/src/generators/setup-mfe/schema.json @@ -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." diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts index af8946ab25..25b3d2b26d 100644 --- a/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts @@ -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(); + }); }); diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.ts index b20c12c6c5..00a411774e 100644 --- a/packages/angular/src/generators/setup-mfe/setup-mfe.ts +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.ts @@ -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); diff --git a/scripts/depcheck/discrepancies.ts b/scripts/depcheck/discrepancies.ts index df24244c5a..b41cd6a93c 100644 --- a/scripts/depcheck/discrepancies.ts +++ b/scripts/depcheck/discrepancies.ts @@ -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( diff --git a/yarn.lock b/yarn.lock index 13ffbe5e1d..731664f82a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==