From 6c9995939deb53bab3850060467d0c4986a802d2 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 8 Mar 2022 14:14:21 +0000 Subject: [PATCH] feat(angular): add mfe-remote generator (#9191) --- .../api-angular/generators/mfe-remote.md | 60 ++++++ docs/map.json | 5 + packages/angular/generators.json | 12 ++ packages/angular/generators.ts | 1 + .../__snapshots__/mfe-remote.spec.ts.snap | 192 ++++++++++++++++++ .../mfe-remote/mfe-remote.compat.ts | 4 + .../generators/mfe-remote/mfe-remote.spec.ts | 61 ++++++ .../src/generators/mfe-remote/mfe-remote.ts | 24 +++ .../src/generators/mfe-remote/schema.d.ts | 5 + .../src/generators/mfe-remote/schema.json | 33 +++ 10 files changed, 397 insertions(+) create mode 100644 docs/generated/api-angular/generators/mfe-remote.md create mode 100644 packages/angular/src/generators/mfe-remote/__snapshots__/mfe-remote.spec.ts.snap create mode 100644 packages/angular/src/generators/mfe-remote/mfe-remote.compat.ts create mode 100644 packages/angular/src/generators/mfe-remote/mfe-remote.spec.ts create mode 100644 packages/angular/src/generators/mfe-remote/mfe-remote.ts create mode 100644 packages/angular/src/generators/mfe-remote/schema.d.ts create mode 100644 packages/angular/src/generators/mfe-remote/schema.json diff --git a/docs/generated/api-angular/generators/mfe-remote.md b/docs/generated/api-angular/generators/mfe-remote.md new file mode 100644 index 0000000000000..29f39af01d1cb --- /dev/null +++ b/docs/generated/api-angular/generators/mfe-remote.md @@ -0,0 +1,60 @@ +--- +title: '@nrwl/angular:mfe-remote generator' +description: 'Generate a Remote Angular Micro-Frontend Application.' +--- + +# @nrwl/angular:mfe-remote + +Generate a Remote Angular Micro-Frontend Application. + +## Usage + +```bash +nx generate mfe-remote ... +``` + +```bash +nx g remote ... # same +``` + +By default, Nx will search for `mfe-remote` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:mfe-remote ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g mfe-remote ... --dry-run +``` + +### Examples + +Create an Angular app with configuration in place for MFE. If host is provided, attach this remote app to host app's configuration.: + +```bash +nx g @nrwl/angular:mfe-remote appName --host=host --port=4201 +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name to give to the remote Angular app. + +### host + +Type: `string` + +The name of the host app to attach this remote 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 5f7b998065dbe..9b614330ec382 100644 --- a/docs/map.json +++ b/docs/map.json @@ -643,6 +643,11 @@ "id": "mfe-host", "file": "generated/api-angular/generators/mfe-host" }, + { + "name": "mfe-remote generator", + "id": "mfe-remote", + "file": "generated/api-angular/generators/mfe-remote" + }, { "name": "move generator", "id": "move", diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 9296225f1029e..57df82b7a1a36 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -68,6 +68,12 @@ "aliases": ["secondary-entry-point", "entry-point"], "description": "Creates a secondary entry point for an Angular publishable library." }, + "mfe-remote": { + "factory": "./src/generators/mfe-remote/mfe-remote.compat", + "schema": "./src/generators/mfe-remote/schema.json", + "aliases": ["remote"], + "description": "Generate a Remote Angular Micro-Frontend Application." + }, "move": { "factory": "./src/generators/move/move#angularMoveSchematic", "schema": "./src/generators/move/schema.json", @@ -207,6 +213,12 @@ "aliases": ["secondary-entry-point", "entry-point"], "description": "Creates a secondary entry point for an Angular publishable library." }, + "mfe-remote": { + "factory": "./src/generators/mfe-remote/mfe-remote", + "schema": "./src/generators/mfe-remote/schema.json", + "aliases": ["remote"], + "description": "Generate a Remote Angular Micro-Frontend Application." + }, "move": { "factory": "./src/generators/move/move#angularMoveGenerator", "schema": "./src/generators/move/schema.json", diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index 7427ae12a6105..4bce969fa262c 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -23,3 +23,4 @@ export * from './src/generators/add-linting/add-linting'; export * from './src/generators/component-cypress-spec/component-cypress-spec'; export * from './src/generators/component-story/component-story'; export * from './src/generators/web-worker/web-worker'; +export * from './src/generators/mfe-remote/mfe-remote'; diff --git a/packages/angular/src/generators/mfe-remote/__snapshots__/mfe-remote.spec.ts.snap b/packages/angular/src/generators/mfe-remote/__snapshots__/mfe-remote.spec.ts.snap new file mode 100644 index 0000000000000..7fdd1499ec381 --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/__snapshots__/mfe-remote.spec.ts.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MFE Remote App Generator should generate a remote mfe app with a host 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: \\"host\\", + publicPath: \\"auto\\", + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + remotes: { + \\"test\\": '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 Remote App Generator should generate a remote mfe app with a host 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({ + name: \\"test\\", + filename: \\"remoteEntry.js\\", + exposes: { + './Module': 'apps/test/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 Remote App Generator should generate a remote mfe app with no host 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({ + name: \\"test\\", + filename: \\"remoteEntry.js\\", + exposes: { + './Module': 'apps/test/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(), + ], +}; +" +`; diff --git a/packages/angular/src/generators/mfe-remote/mfe-remote.compat.ts b/packages/angular/src/generators/mfe-remote/mfe-remote.compat.ts new file mode 100644 index 0000000000000..1eaea63f5b31c --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/mfe-remote.compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import mfeRemote from './mfe-remote'; + +export default convertNxGenerator(mfeRemote); diff --git a/packages/angular/src/generators/mfe-remote/mfe-remote.spec.ts b/packages/angular/src/generators/mfe-remote/mfe-remote.spec.ts new file mode 100644 index 0000000000000..716708c49a222 --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/mfe-remote.spec.ts @@ -0,0 +1,61 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import mfeRemote from './mfe-remote'; +import applicationGenerator from '../application/application'; + +describe('MFE Remote App Generator', () => { + it('should generate a remote mfe app with no host', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + // ACT + await mfeRemote(tree, { + name: 'test', + port: 4201, + }); + + // ASSERT + expect(tree.read('apps/test/webpack.config.js', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate a remote mfe app with a host', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + await applicationGenerator(tree, { + name: 'host', + mfe: true, + mfeType: 'host', + routing: true, + }); + + // ACT + await mfeRemote(tree, { + name: 'test', + host: 'host', + port: 4201, + }); + + // ASSERT + expect(tree.read('apps/host/webpack.config.js', 'utf-8')).toMatchSnapshot(); + expect(tree.read('apps/test/webpack.config.js', 'utf-8')).toMatchSnapshot(); + }); + + it('should error when a remote app is attempted to be generated with an incorrect host', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + + // ACT + try { + await mfeRemote(tree, { + name: 'test', + host: 'host', + port: 4201, + }); + } catch (error) { + // ASSERT + expect(error.message).toEqual( + 'The name of the application to be used as the host app does not exist. (host)' + ); + } + }); +}); diff --git a/packages/angular/src/generators/mfe-remote/mfe-remote.ts b/packages/angular/src/generators/mfe-remote/mfe-remote.ts new file mode 100644 index 0000000000000..f4308dff490b5 --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/mfe-remote.ts @@ -0,0 +1,24 @@ +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 mfeRemote(tree: Tree, options: Schema) { + const projects = getProjects(tree); + if (options.host && !projects.has(options.host)) { + throw new Error( + `The name of the application to be used as the host app does not exist. (${options.host})` + ); + } + + const installTask = await applicationGenerator(tree, { + name: options.name, + mfe: true, + mfeType: 'remote', + routing: true, + host: options.host, + port: options.port ?? 4200, + }); + + return installTask; +} diff --git a/packages/angular/src/generators/mfe-remote/schema.d.ts b/packages/angular/src/generators/mfe-remote/schema.d.ts new file mode 100644 index 0000000000000..5a11cede74742 --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/schema.d.ts @@ -0,0 +1,5 @@ +export interface Schema { + name: string; + host?: string; + port?: number; +} diff --git a/packages/angular/src/generators/mfe-remote/schema.json b/packages/angular/src/generators/mfe-remote/schema.json new file mode 100644 index 0000000000000..eb7f996aa402a --- /dev/null +++ b/packages/angular/src/generators/mfe-remote/schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxMFERemote", + "cli": "nx", + "title": "Nx MFE Remote App", + "description": "Create an Angular Remote Micro Frontend Application", + "type": "object", + "examples": [ + { + "command": "g @nrwl/angular:mfe-remote appName --host=host --port=4201", + "description": "Create an Angular app with configuration in place for MFE. If host is provided, attach this remote app to host app's configuration." + } + ], + "properties": { + "name": { + "type": "string", + "description": "The name to give to the remote Angular app.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "host": { + "type": "string", + "description": "The name of the host app to attach this remote app to." + }, + "port": { + "type": "string", + "description": "The port on which this app should be served." + } + }, + "required": ["name"] +}