From 744203057d3fc9d0b75901b9fe0a1308491ca35e Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 8 Mar 2022 13:30:24 +0000 Subject: [PATCH] feat(angular): add mfe-host generator (#9218) --- .../api-angular/generators/mfe-host.md | 60 ++++++ docs/map.json | 5 + packages/angular/generators.json | 12 ++ packages/angular/generators.ts | 1 + .../__snapshots__/mfe-host.spec.ts.snap | 189 ++++++++++++++++++ .../generators/mfe-host/mfe-host.compat.ts | 4 + .../src/generators/mfe-host/mfe-host.spec.ts | 61 ++++++ .../src/generators/mfe-host/mfe-host.ts | 30 +++ .../src/generators/mfe-host/schema.d.ts | 4 + .../src/generators/mfe-host/schema.json | 33 +++ .../setup-mfe/lib/get-remotes-with-ports.ts | 2 +- 11 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 docs/generated/api-angular/generators/mfe-host.md create mode 100644 packages/angular/src/generators/mfe-host/__snapshots__/mfe-host.spec.ts.snap create mode 100644 packages/angular/src/generators/mfe-host/mfe-host.compat.ts create mode 100644 packages/angular/src/generators/mfe-host/mfe-host.spec.ts create mode 100644 packages/angular/src/generators/mfe-host/mfe-host.ts create mode 100644 packages/angular/src/generators/mfe-host/schema.d.ts create mode 100644 packages/angular/src/generators/mfe-host/schema.json diff --git a/docs/generated/api-angular/generators/mfe-host.md b/docs/generated/api-angular/generators/mfe-host.md new file mode 100644 index 0000000000000..373610cc48403 --- /dev/null +++ b/docs/generated/api-angular/generators/mfe-host.md @@ -0,0 +1,60 @@ +--- +title: '@nrwl/angular:mfe-host generator' +description: 'Generate a Host Angular Micro Frontend Application.' +--- + +# @nrwl/angular:mfe-host + +Generate a Host Angular Micro Frontend Application. + +## Usage + +```bash +nx generate mfe-host ... +``` + +```bash +nx g host ... # same +``` + +By default, Nx will search for `mfe-host` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:mfe-host ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g mfe-host ... --dry-run +``` + +### Examples + +Create an Angular app with configuration in place for MFE. If remotes is provided, attach the remote app to this app's configuration.: + +```bash +nx g @nrwl/angular:mfe-host appName --remotes=remote1 +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name to give to the host Angular app. + +### host + +Type: `string` + +The name of the host app to attach this host app to. + +### port + +Type: `string` + +The port on which this app should be served. diff --git a/docs/map.json b/docs/map.json index 51d6916309220..5f7b998065dbe 100644 --- a/docs/map.json +++ b/docs/map.json @@ -638,6 +638,11 @@ "id": "library-secondary-entry-point", "file": "generated/api-angular/generators/library-secondary-entry-point" }, + { + "name": "mfe-host generator", + "id": "mfe-host", + "file": "generated/api-angular/generators/mfe-host" + }, { "name": "move generator", "id": "move", diff --git a/packages/angular/generators.json b/packages/angular/generators.json index bb792d026568d..9296225f1029e 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -74,6 +74,12 @@ "aliases": ["mv"], "description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration." }, + "mfe-host": { + "factory": "./src/generators/mfe-host/mfe-host.compat", + "schema": "./src/generators/mfe-host/schema.json", + "aliases": ["host"], + "description": "Generate a Host Angular Micro Frontend Application." + }, "ngrx": { "factory": "./src/generators/ngrx/compat", "schema": "./src/generators/ngrx/schema.json", @@ -207,6 +213,12 @@ "aliases": ["mv"], "description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration." }, + "mfe-host": { + "factory": "./src/generators/mfe-host/mfe-host", + "schema": "./src/generators/mfe-host/schema.json", + "aliases": ["host"], + "description": "Generate a Host Angular Micro Frontend Application." + }, "ngrx": { "factory": "./src/generators/ngrx/ngrx", "schema": "./src/generators/ngrx/schema.json", diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index 9f7b69c36c1b6..7427ae12a6105 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -15,6 +15,7 @@ export * from './src/generators/storybook-migrate-defaults-5-to-6/storybook-migr export * from './src/generators/storybook-migrate-stories-to-6-2/migrate-stories-to-6-2'; export * from './src/generators/upgrade-module/upgrade-module'; export * from './src/generators/setup-mfe/setup-mfe'; +export * from './src/generators/mfe-host/mfe-host'; export * from './src/generators/scam/scam'; export * from './src/generators/scam-directive/scam-directive'; export * from './src/generators/scam-pipe/scam-pipe'; diff --git a/packages/angular/src/generators/mfe-host/__snapshots__/mfe-host.spec.ts.snap b/packages/angular/src/generators/mfe-host/__snapshots__/mfe-host.spec.ts.snap new file mode 100644 index 0000000000000..88c472590e307 --- /dev/null +++ b/packages/angular/src/generators/mfe-host/__snapshots__/mfe-host.spec.ts.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MFE Host App Generator should generate a host mfe app with a remote 1`] = ` +"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\"); +const mf = require(\\"@angular-architects/module-federation/webpack\\"); +const path = require(\\"path\\"); +const share = mf.share; + +/** +* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser +* builder as it will generate a temporary tsconfig file which contains any required remappings of +* shared libraries. +* A remapping will occur when a library is buildable, as webpack needs to know the location of the +* built files for the buildable library. +* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains +* the location of the generated temporary tsconfig file. +*/ +const tsConfigPath = process.env.NX_TSCONFIG_PATH ?? path.join(__dirname, '../../tsconfig.base.json'); + +const workspaceRootPath = path.join(__dirname, '../../'); +const sharedMappings = new mf.SharedMappings(); +sharedMappings.register(tsConfigPath, [ + /* mapped paths to share */ +], workspaceRootPath); + +module.exports = { + output: { + uniqueName: \\"remote\\", + publicPath: \\"auto\\", + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + name: \\"remote\\", + filename: \\"remoteEntry.js\\", + exposes: { + './Module': 'apps/remote/src/app/remote-entry/entry.module.ts', + }, + shared: share({ + \\"@angular/core\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common/http\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/router\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"rxjs\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + ...sharedMappings.getDescriptors(), + }), + library: { + type: 'module' + }, + }), + sharedMappings.getPlugin(), + ], +}; +" +`; + +exports[`MFE Host App Generator should generate a host mfe app with a remote 2`] = ` +"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\"); +const mf = require(\\"@angular-architects/module-federation/webpack\\"); +const path = require(\\"path\\"); +const share = mf.share; + +/** +* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser +* builder as it will generate a temporary tsconfig file which contains any required remappings of +* shared libraries. +* A remapping will occur when a library is buildable, as webpack needs to know the location of the +* built files for the buildable library. +* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains +* the location of the generated temporary tsconfig file. +*/ +const tsConfigPath = process.env.NX_TSCONFIG_PATH ?? path.join(__dirname, '../../tsconfig.base.json'); + +const workspaceRootPath = path.join(__dirname, '../../'); +const sharedMappings = new mf.SharedMappings(); +sharedMappings.register(tsConfigPath, [ + /* mapped paths to share */ +], workspaceRootPath); + +module.exports = { + output: { + uniqueName: \\"test\\", + publicPath: \\"auto\\", + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + remotes: { + \\"remote\\": \\"http://localhost:4201/remoteEntry.js\\", + }, + shared: share({ + \\"@angular/core\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common/http\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/router\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"rxjs\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + ...sharedMappings.getDescriptors(), + }), + library: { + type: 'module' + }, + }), + sharedMappings.getPlugin(), + ], +}; +" +`; + +exports[`MFE Host App Generator should generate a host mfe app with no remotes 1`] = ` +"const ModuleFederationPlugin = require(\\"webpack/lib/container/ModuleFederationPlugin\\"); +const mf = require(\\"@angular-architects/module-federation/webpack\\"); +const path = require(\\"path\\"); +const share = mf.share; + +/** +* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser +* builder as it will generate a temporary tsconfig file which contains any required remappings of +* shared libraries. +* A remapping will occur when a library is buildable, as webpack needs to know the location of the +* built files for the buildable library. +* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains +* the location of the generated temporary tsconfig file. +*/ +const tsConfigPath = process.env.NX_TSCONFIG_PATH ?? path.join(__dirname, '../../tsconfig.base.json'); + +const workspaceRootPath = path.join(__dirname, '../../'); +const sharedMappings = new mf.SharedMappings(); +sharedMappings.register(tsConfigPath, [ + /* mapped paths to share */ +], workspaceRootPath); + +module.exports = { + output: { + uniqueName: \\"test\\", + publicPath: \\"auto\\", + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + remotes: { + + }, + shared: share({ + \\"@angular/core\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/common/http\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"@angular/router\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + \\"rxjs\\": { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + ...sharedMappings.getDescriptors(), + }), + library: { + type: 'module' + }, + }), + sharedMappings.getPlugin(), + ], +}; +" +`; diff --git a/packages/angular/src/generators/mfe-host/mfe-host.compat.ts b/packages/angular/src/generators/mfe-host/mfe-host.compat.ts new file mode 100644 index 0000000000000..c9cb30727690e --- /dev/null +++ b/packages/angular/src/generators/mfe-host/mfe-host.compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import mfeHost from './mfe-host'; + +export default convertNxGenerator(mfeHost); diff --git a/packages/angular/src/generators/mfe-host/mfe-host.spec.ts b/packages/angular/src/generators/mfe-host/mfe-host.spec.ts new file mode 100644 index 0000000000000..c57f76c7c61f2 --- /dev/null +++ b/packages/angular/src/generators/mfe-host/mfe-host.spec.ts @@ -0,0 +1,61 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import mfeHost from './mfe-host'; +import applicationGenerator from '../application/application'; + +describe('MFE Host App Generator', () => { + it('should generate a host mfe app with no remotes', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + // ACT + await mfeHost(tree, { + name: 'test', + }); + + // ASSERT + expect(tree.read('apps/test/webpack.config.js', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate a host mfe app with a remote', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + await applicationGenerator(tree, { + name: 'remote', + mfe: true, + mfeType: 'remote', + routing: true, + port: 4201, + }); + + // ACT + await mfeHost(tree, { + name: 'test', + remotes: ['remote'], + }); + + // ASSERT + expect( + tree.read('apps/remote/webpack.config.js', 'utf-8') + ).toMatchSnapshot(); + expect(tree.read('apps/test/webpack.config.js', 'utf-8')).toMatchSnapshot(); + }); + + it('should error when a host app is attempted to be generated with an incorrect remote', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + // ACT + try { + await mfeHost(tree, { + name: 'test', + remotes: ['remote'], + }); + } catch (error) { + // ASSERT + expect(error.message).toEqual( + 'Could not find specified remote application (remote)' + ); + } + }); +}); diff --git a/packages/angular/src/generators/mfe-host/mfe-host.ts b/packages/angular/src/generators/mfe-host/mfe-host.ts new file mode 100644 index 0000000000000..e7922607bee5f --- /dev/null +++ b/packages/angular/src/generators/mfe-host/mfe-host.ts @@ -0,0 +1,30 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from './schema'; + +import { getProjects } from '@nrwl/devkit'; +import applicationGenerator from '../application/application'; + +export default async function mfeHost(tree: Tree, options: Schema) { + const projects = getProjects(tree); + + if (options.remotes && options.remotes.length > 0) { + options.remotes.forEach((remote) => { + if (!projects.has(remote)) { + throw new Error( + `Could not find specified remote application (${remote})` + ); + } + }); + } + + const installTask = await applicationGenerator(tree, { + name: options.name, + mfe: true, + mfeType: 'host', + routing: true, + remotes: options.remotes ?? [], + port: 4200, + }); + + return installTask; +} diff --git a/packages/angular/src/generators/mfe-host/schema.d.ts b/packages/angular/src/generators/mfe-host/schema.d.ts new file mode 100644 index 0000000000000..4f3d7cacad251 --- /dev/null +++ b/packages/angular/src/generators/mfe-host/schema.d.ts @@ -0,0 +1,4 @@ +export interface Schema { + name: string; + remotes?: string[]; +} diff --git a/packages/angular/src/generators/mfe-host/schema.json b/packages/angular/src/generators/mfe-host/schema.json new file mode 100644 index 0000000000000..afa31d822f4dd --- /dev/null +++ b/packages/angular/src/generators/mfe-host/schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxMFEHost", + "cli": "nx", + "title": "Nx MFE Host App", + "description": "Create an Angular Host Micro Frontend Application", + "type": "object", + "examples": [ + { + "command": "g @nrwl/angular:mfe-host appName --remotes=remote1", + "description": "Create an Angular app with configuration in place for MFE. If remotes is provided, attach the remote app to this app's configuration." + } + ], + "properties": { + "name": { + "type": "string", + "description": "The name to give to the host Angular app.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "host": { + "type": "string", + "description": "The name of the host app to attach this host app to." + }, + "port": { + "type": "string", + "description": "The port on which this app should be served." + } + }, + "required": ["name"] +} diff --git a/packages/angular/src/generators/setup-mfe/lib/get-remotes-with-ports.ts b/packages/angular/src/generators/setup-mfe/lib/get-remotes-with-ports.ts index 0eb049b8f4b04..eefcb20543e5a 100644 --- a/packages/angular/src/generators/setup-mfe/lib/get-remotes-with-ports.ts +++ b/packages/angular/src/generators/setup-mfe/lib/get-remotes-with-ports.ts @@ -15,7 +15,7 @@ export function getRemotesWithPorts(host: Tree, options: Schema) { const remoteConfig = readProjectConfiguration(host, remote); remotesWithPort.push({ remoteName: remote, - port: remoteConfig.targets['mfe-serve']?.options.port ?? 4200, + port: remoteConfig.targets['serve']?.options?.port ?? 4200, }); } }