From 5378128851eaea8a130eb7d1b94a3326453d5015 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 29 Mar 2022 15:45:19 +0000 Subject: [PATCH] feat(angular): add dynamic federation support to mfe generator (#9551) --- docs/generated/packages/angular.json | 17 +++ packages/angular/mfe/index.ts | 6 +- .../src/generators/application/lib/add-mfe.ts | 1 + .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 6 + .../src/generators/mfe-host/mfe-host.ts | 1 + .../src/generators/mfe-host/schema.d.ts | 1 + .../src/generators/mfe-host/schema.json | 5 + .../__snapshots__/setup-mfe.spec.ts.snap | 37 +++++ .../setup-mfe/lib/add-remote-to-host.ts | 139 ++++++++++++----- .../generators/setup-mfe/lib/fix-bootstrap.ts | 22 ++- .../src/generators/setup-mfe/lib/index.ts | 1 + .../setup-mfe/lib/setup-host-if-dynamic.ts | 19 +++ .../src/generators/setup-mfe/schema.d.ts | 1 + .../src/generators/setup-mfe/schema.json | 6 + .../generators/setup-mfe/setup-mfe.spec.ts | 143 ++++++++++++------ .../src/generators/setup-mfe/setup-mfe.ts | 31 ++-- 17 files changed, 334 insertions(+), 103 deletions(-) create mode 100644 packages/angular/src/generators/setup-mfe/lib/setup-host-if-dynamic.ts diff --git a/docs/generated/packages/angular.json b/docs/generated/packages/angular.json index 22cca3bf9cdd6..149ff2a31b3d7 100644 --- a/docs/generated/packages/angular.json +++ b/docs/generated/packages/angular.json @@ -191,6 +191,12 @@ "description": "Type of application to generate the Module Federation configuration for.", "default": "remote" }, + "federationType": { + "type": "string", + "enum": ["static", "dynamic"], + "description": "Use either Static or Dynamic Module Federation pattern for the application.", + "default": "static" + }, "port": { "type": "number", "description": "The port at which the remote application should be served." @@ -1004,6 +1010,11 @@ "port": { "type": "number", "description": "The port on which this app should be served." + }, + "dynamic": { + "type": "boolean", + "description": "Should the host app use dynamic federation?", + "default": false } }, "required": ["name"], @@ -1454,6 +1465,12 @@ "description": "Type of application to generate the Module Federation configuration for.", "default": "remote" }, + "federationType": { + "type": "string", + "enum": ["static", "dynamic"], + "description": "Use either Static or Dynamic Module Federation pattern for the application.", + "default": "static" + }, "port": { "type": "number", "description": "The port at which the remote application should be served." diff --git a/packages/angular/mfe/index.ts b/packages/angular/mfe/index.ts index e4e04b459af09..2902e00ddeac9 100644 --- a/packages/angular/mfe/index.ts +++ b/packages/angular/mfe/index.ts @@ -1 +1,5 @@ -export { setRemoteUrlResolver, loadRemoteModule } from './mfe'; +export { + setRemoteUrlResolver, + setRemoteDefinitions, + loadRemoteModule, +} from './mfe'; diff --git a/packages/angular/src/generators/application/lib/add-mfe.ts b/packages/angular/src/generators/application/lib/add-mfe.ts index 3f840e762b9d0..4310fa0db159c 100644 --- a/packages/angular/src/generators/application/lib/add-mfe.ts +++ b/packages/angular/src/generators/application/lib/add-mfe.ts @@ -14,5 +14,6 @@ export async function addMfe(host: Tree, options: NormalizedSchema) { skipFormat: true, skipPackageJson: options.skipPackageJson, e2eProjectName: options.e2eProjectName, + federationType: options.federationType, }); } diff --git a/packages/angular/src/generators/application/schema.d.ts b/packages/angular/src/generators/application/schema.d.ts index 43e2fc003788f..92c26efdfdafd 100644 --- a/packages/angular/src/generators/application/schema.d.ts +++ b/packages/angular/src/generators/application/schema.d.ts @@ -30,4 +30,5 @@ export interface Schema { host?: string; setParserOptionsProject?: boolean; skipPackageJson?: boolean; + federationType?: 'static' | 'dynamic'; } diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index b0eaa54fa61ef..c6e472c0eea56 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -138,6 +138,12 @@ "description": "Type of application to generate the Module Federation configuration for.", "default": "remote" }, + "federationType": { + "type": "string", + "enum": ["static", "dynamic"], + "description": "Use either Static or Dynamic Module Federation pattern for the application.", + "default": "static" + }, "port": { "type": "number", "description": "The port at which the remote application should be served." diff --git a/packages/angular/src/generators/mfe-host/mfe-host.ts b/packages/angular/src/generators/mfe-host/mfe-host.ts index e7922607bee5f..0c1b6cc139796 100644 --- a/packages/angular/src/generators/mfe-host/mfe-host.ts +++ b/packages/angular/src/generators/mfe-host/mfe-host.ts @@ -24,6 +24,7 @@ export default async function mfeHost(tree: Tree, options: Schema) { routing: true, remotes: options.remotes ?? [], port: 4200, + federationType: options.dynamic ? 'dynamic' : 'static', }); return installTask; diff --git a/packages/angular/src/generators/mfe-host/schema.d.ts b/packages/angular/src/generators/mfe-host/schema.d.ts index 4f3d7cacad251..1c3a1a497f1af 100644 --- a/packages/angular/src/generators/mfe-host/schema.d.ts +++ b/packages/angular/src/generators/mfe-host/schema.d.ts @@ -1,4 +1,5 @@ export interface Schema { name: string; remotes?: string[]; + dynamic?: boolean; } diff --git a/packages/angular/src/generators/mfe-host/schema.json b/packages/angular/src/generators/mfe-host/schema.json index 0577c0c6467dc..27f9d847780e0 100644 --- a/packages/angular/src/generators/mfe-host/schema.json +++ b/packages/angular/src/generators/mfe-host/schema.json @@ -27,6 +27,11 @@ "port": { "type": "number", "description": "The port on which this app should be served." + }, + "dynamic": { + "type": "boolean", + "description": "Should the host app use dynamic federation?", + "default": false } }, "required": ["name"] 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 240c15d367ec4..ccdfde0756cd3 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,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Init MFE --federationType=dynamic should create a host with the correct configurations 1`] = ` +"import { setRemoteDefinitions } from '@nrwl/angular/mfe'; + + fetch('/assets/mfe.manifest.json') + .then((res) => res.json()) + .then(definitions => setRemoteDefinitions(definitions)) + .then(() => import('./bootstrap').catch(err => console.error(err)))" +`; + exports[`Init MFE should add a remote application and add it to a specified host applications router config 1`] = ` "import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @@ -44,6 +53,34 @@ exports[`Init MFE should add a remote application and add it to a specified host }" `; +exports[`Init MFE should add a remote to dynamic host correctly 1`] = ` +"import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { NxWelcomeComponent } from './nx-welcome.component'; +import { RouterModule } from '@angular/router'; +import { loadRemoteModule } from '@nrwl/angular/mfe'; + +@NgModule({ + declarations: [ + AppComponent, + NxWelcomeComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot([{ + path: 'remote1', + loadChildren: () => loadRemoteModule('remote1', './Module').then(m => m.RemoteEntryModule) + }], {initialNavigation: 'enabledBlocking'}) + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } +" +`; + exports[`Init MFE should create webpack and mfe configs correctly 1`] = ` "const { withModuleFederation } = require('@nrwl/angular/module-federation'); const config = require('./mfe.config'); 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 index 732efb4e069e3..672f00a77f48b 100644 --- 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 @@ -1,12 +1,16 @@ -import type { Tree } from '@nrwl/devkit'; +import { ProjectConfiguration, Tree, updateJson } from '@nrwl/devkit'; import type { Schema } from '../schema'; import { readProjectConfiguration, joinPathFragments } from '@nrwl/devkit'; import { tsquery } from '@phenomnomnominal/tsquery'; import { ArrayLiteralExpression } from 'typescript'; -import { addRoute } from '../../../utils/nx-devkit/ast-utils'; +import { + addImportToModule, + addRoute, +} from '../../../utils/nx-devkit/ast-utils'; import * as ts from 'typescript'; +import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils'; export function checkIsCommaNeeded(mfeRemoteText: string) { const remoteText = mfeRemoteText.replace(/\s+/g, ''); @@ -17,61 +21,106 @@ export function checkIsCommaNeeded(mfeRemoteText: string) { : false; } -export function addRemoteToHost(host: Tree, options: Schema) { +export function addRemoteToHost(tree: Tree, options: Schema) { if (options.mfeType === 'remote' && options.host) { - const hostProject = readProjectConfiguration(host, options.host); - const hostMfeConfigPath = joinPathFragments( - hostProject.root, - 'mfe.config.js' + const hostProject = readProjectConfiguration(tree, options.host); + const pathToMfeManifest = joinPathFragments( + hostProject.sourceRoot, + 'assets/mfe.manifest.json' + ); + const hostFederationType = determineHostFederationType( + tree, + pathToMfeManifest ); - if (!hostMfeConfigPath || !host.exists(hostMfeConfigPath)) { - throw new Error( - `The selected host application, ${options.host}, does not contain a mfe.config.js. Are you sure it has been set up as a host application?` - ); + if (hostFederationType === 'static') { + addRemoteToStaticHost(tree, options, hostProject); + } else if (hostFederationType === 'dynamic') { + addRemoteToDynamicHost(tree, options, pathToMfeManifest); } - const hostMFEConfig = host.read(hostMfeConfigPath, 'utf-8'); - const webpackAst = tsquery.ast(hostMFEConfig); - const mfRemotesNode = tsquery( - webpackAst, - 'Identifier[name=remotes] ~ ArrayLiteralExpression', - { visitAllChildren: true } - )[0] as ArrayLiteralExpression; - - const endOfPropertiesPos = mfRemotesNode.getEnd() - 1; - const isCommaNeeded = checkIsCommaNeeded(mfRemotesNode.getText()); - - const updatedConfig = `${hostMFEConfig.slice(0, endOfPropertiesPos)}${ - isCommaNeeded ? ',' : '' - }'${options.appName}',${hostMFEConfig.slice(endOfPropertiesPos)}`; - - host.write(hostMfeConfigPath, updatedConfig); - const declarationFilePath = joinPathFragments( hostProject.sourceRoot, 'decl.d.ts' ); const declarationFileContent = - (host.exists(declarationFilePath) - ? host.read(declarationFilePath, 'utf-8') + (tree.exists(declarationFilePath) + ? tree.read(declarationFilePath, 'utf-8') : '') + `\ndeclare module '${options.appName}/Module';`; - host.write(declarationFilePath, declarationFileContent); + tree.write(declarationFilePath, declarationFileContent); - addLazyLoadedRouteToHostAppModule(host, options); + addLazyLoadedRouteToHostAppModule(tree, options, hostFederationType); } } +function determineHostFederationType( + tree: Tree, + pathToMfeManifest: string +): 'dynamic' | 'static' { + return tree.exists(pathToMfeManifest) ? 'dynamic' : 'static'; +} + +function addRemoteToStaticHost( + tree: Tree, + options: Schema, + hostProject: ProjectConfiguration +) { + const hostMfeConfigPath = joinPathFragments( + hostProject.root, + 'mfe.config.js' + ); + + if (!hostMfeConfigPath || !tree.exists(hostMfeConfigPath)) { + throw new Error( + `The selected host application, ${options.host}, does not contain a mfe.config.js or mfe.manifest.json file. Are you sure it has been set up as a host application?` + ); + } + + const hostMFEConfig = tree.read(hostMfeConfigPath, 'utf-8'); + const webpackAst = tsquery.ast(hostMFEConfig); + const mfRemotesNode = tsquery( + webpackAst, + 'Identifier[name=remotes] ~ ArrayLiteralExpression', + { visitAllChildren: true } + )[0] as ArrayLiteralExpression; + + const endOfPropertiesPos = mfRemotesNode.getEnd() - 1; + const isCommaNeeded = checkIsCommaNeeded(mfRemotesNode.getText()); + + const updatedConfig = `${hostMFEConfig.slice(0, endOfPropertiesPos)}${ + isCommaNeeded ? ',' : '' + }'${options.appName}',${hostMFEConfig.slice(endOfPropertiesPos)}`; + + tree.write(hostMfeConfigPath, updatedConfig); +} + +function addRemoteToDynamicHost( + tree: Tree, + options: Schema, + pathToMfeManifest: string +) { + updateJson(tree, pathToMfeManifest, (manifest) => { + return { + ...manifest, + [options.appName]: `http://localhost:${options.port}`, + }; + }); +} + // TODO(colum): future work: allow dev to pass to path to routing module -function addLazyLoadedRouteToHostAppModule(host: Tree, options: Schema) { - const hostAppConfig = readProjectConfiguration(host, options.host); +function addLazyLoadedRouteToHostAppModule( + tree: Tree, + options: Schema, + hostFederationType: 'dynamic' | 'static' +) { + const hostAppConfig = readProjectConfiguration(tree, options.host); const pathToHostAppModule = `${hostAppConfig.sourceRoot}/app/app.module.ts`; - if (!host.exists(pathToHostAppModule)) { + if (!tree.exists(pathToHostAppModule)) { return; } - const hostAppModule = host.read(pathToHostAppModule, 'utf-8'); + const hostAppModule = tree.read(pathToHostAppModule, 'utf-8'); if (!hostAppModule.includes('RouterModule.forRoot(')) { return; } @@ -83,13 +132,27 @@ function addLazyLoadedRouteToHostAppModule(host: Tree, options: Schema) { true ); + if (hostFederationType === 'dynamic') { + sourceFile = insertImport( + tree, + sourceFile, + pathToHostAppModule, + 'loadRemoteModule', + '@nrwl/angular/mfe' + ); + } + const routeToAdd = + hostFederationType === 'dynamic' + ? `loadRemoteModule('${options.appName}', './Module')` + : `import('${options.appName}/Module')`; + sourceFile = addRoute( - host, + tree, pathToHostAppModule, sourceFile, `{ path: '${options.appName}', - loadChildren: () => import('${options.appName}/Module').then(m => m.RemoteEntryModule) + loadChildren: () => ${routeToAdd}.then(m => m.RemoteEntryModule) }` ); } diff --git a/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts b/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts index 120fa5bcf537d..3877911aba453 100644 --- a/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts +++ b/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts @@ -1,14 +1,26 @@ import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; import { joinPathFragments } from '@nrwl/devkit'; -export function fixBootstrap(host: Tree, appRoot: string) { +export function fixBootstrap(tree: Tree, appRoot: string, options: Schema) { const mainFilePath = joinPathFragments(appRoot, 'src/main.ts'); - const bootstrapCode = host.read(mainFilePath, 'utf-8'); - host.write(joinPathFragments(appRoot, 'src/bootstrap.ts'), bootstrapCode); + const bootstrapCode = tree.read(mainFilePath, 'utf-8'); + tree.write(joinPathFragments(appRoot, 'src/bootstrap.ts'), bootstrapCode); - host.write( + const bootstrapImportCode = `import('./bootstrap').catch(err => console.error(err))`; + + const fetchMfeManifestCode = `import { setRemoteDefinitions } from '@nrwl/angular/mfe'; + + fetch('/assets/mfe.manifest.json') + .then((res) => res.json()) + .then(definitions => setRemoteDefinitions(definitions)) + .then(() => ${bootstrapImportCode})`; + + tree.write( mainFilePath, - `import('./bootstrap').catch(err => console.error(err));` + options.mfeType === 'host' && options.federationType === 'dynamic' + ? fetchMfeManifestCode + : bootstrapImportCode ); } diff --git a/packages/angular/src/generators/setup-mfe/lib/index.ts b/packages/angular/src/generators/setup-mfe/lib/index.ts index 3c3c6522cd6df..b584f8d7a47e6 100644 --- a/packages/angular/src/generators/setup-mfe/lib/index.ts +++ b/packages/angular/src/generators/setup-mfe/lib/index.ts @@ -7,4 +7,5 @@ export * from './fix-bootstrap'; export * from './generate-config'; export * from './get-remotes-with-ports'; export * from './set-tsconfig-target'; +export * from './setup-host-if-dynamic'; export * from './setup-serve-target'; diff --git a/packages/angular/src/generators/setup-mfe/lib/setup-host-if-dynamic.ts b/packages/angular/src/generators/setup-mfe/lib/setup-host-if-dynamic.ts new file mode 100644 index 0000000000000..41f4ac066a3e8 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/setup-host-if-dynamic.ts @@ -0,0 +1,19 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { readProjectConfiguration, joinPathFragments } from '@nrwl/devkit'; + +export function setupHostIfDynamic(tree: Tree, options: Schema) { + if (options.federationType === 'static' || options.mfeType === 'remote') { + return; + } + + const pathToMfeManifest = joinPathFragments( + readProjectConfiguration(tree, options.appName).sourceRoot, + 'assets/mfe.manifest.json' + ); + + if (!tree.exists(pathToMfeManifest)) { + tree.write(pathToMfeManifest, '{}'); + } +} diff --git a/packages/angular/src/generators/setup-mfe/schema.d.ts b/packages/angular/src/generators/setup-mfe/schema.d.ts index 3ec8216078483..825e2e76d390a 100644 --- a/packages/angular/src/generators/setup-mfe/schema.d.ts +++ b/packages/angular/src/generators/setup-mfe/schema.d.ts @@ -4,6 +4,7 @@ export interface Schema { port?: number; remotes?: string[]; host?: string; + federationType?: 'static' | 'dynamic'; routing?: boolean; skipFormat?: boolean; skipPackageJson?: boolean; diff --git a/packages/angular/src/generators/setup-mfe/schema.json b/packages/angular/src/generators/setup-mfe/schema.json index 9000a0f3a4233..b4da1bf9ec786 100644 --- a/packages/angular/src/generators/setup-mfe/schema.json +++ b/packages/angular/src/generators/setup-mfe/schema.json @@ -21,6 +21,12 @@ "description": "Type of application to generate the Module Federation configuration for.", "default": "remote" }, + "federationType": { + "type": "string", + "enum": ["static", "dynamic"], + "description": "Use either Static or Dynamic Module Federation pattern for the application.", + "default": "static" + }, "port": { "type": "number", "description": "The port at which the remote application should be served." 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 081c833765ed0..dd280acc10c64 100644 --- a/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts @@ -1,19 +1,19 @@ -import type { ProjectConfiguration, Tree } from '@nrwl/devkit'; +import { ProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; import { readProjectConfiguration } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { setupMfe } from './setup-mfe'; import applicationGenerator from '../application/application'; describe('Init MFE', () => { - let host: Tree; + let tree: Tree; beforeEach(async () => { - host = createTreeWithEmptyWorkspace(); - await applicationGenerator(host, { + tree = createTreeWithEmptyWorkspace(); + await applicationGenerator(tree, { name: 'app1', routing: true, }); - await applicationGenerator(host, { + await applicationGenerator(tree, { name: 'remote1', routing: true, }); @@ -26,23 +26,23 @@ describe('Init MFE', () => { 'should create webpack and mfe configs correctly', async (app, type: 'host' | 'remote') => { // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: app, mfeType: type, }); // ASSERT - expect(host.exists(`apps/${app}/mfe.config.js`)).toBeTruthy(); - expect(host.exists(`apps/${app}/webpack.config.js`)).toBeTruthy(); - expect(host.exists(`apps/${app}/webpack.prod.config.js`)).toBeTruthy(); + expect(tree.exists(`apps/${app}/mfe.config.js`)).toBeTruthy(); + expect(tree.exists(`apps/${app}/webpack.config.js`)).toBeTruthy(); + expect(tree.exists(`apps/${app}/webpack.prod.config.js`)).toBeTruthy(); - const webpackContents = host.read( + const webpackContents = tree.read( `apps/${app}/webpack.config.js`, 'utf-8' ); expect(webpackContents).toMatchSnapshot(); - const mfeConfigContents = host.read(`apps/${app}/mfe.config.js`, 'utf-8'); + const mfeConfigContents = tree.read(`apps/${app}/mfe.config.js`, 'utf-8'); expect(mfeConfigContents).toMatchSnapshot(); } ); @@ -54,20 +54,20 @@ describe('Init MFE', () => { 'create bootstrap file with the contents of main.ts', async (app, type: 'host' | 'remote') => { // ARRANGE - const mainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + const mainContents = tree.read(`apps/${app}/src/main.ts`, 'utf-8'); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: app, mfeType: type, }); // ASSERT - const bootstrapContents = host.read( + const bootstrapContents = tree.read( `apps/${app}/src/bootstrap.ts`, 'utf-8' ); - const updatedMainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + const updatedMainContents = tree.read(`apps/${app}/src/main.ts`, 'utf-8'); expect(bootstrapContents).toEqual(mainContents); expect(updatedMainContents).not.toEqual(mainContents); @@ -81,19 +81,19 @@ describe('Init MFE', () => { 'should alter main.ts to import the bootstrap file dynamically', async (app, type: 'host' | 'remote') => { // ARRANGE - const mainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + const mainContents = tree.read(`apps/${app}/src/main.ts`, 'utf-8'); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: app, mfeType: type, }); // ASSERT - const updatedMainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + const updatedMainContents = tree.read(`apps/${app}/src/main.ts`, 'utf-8'); expect(updatedMainContents).toEqual( - `import('./bootstrap').catch(err => console.error(err));` + `import('./bootstrap').catch(err => console.error(err))` ); expect(updatedMainContents).not.toEqual(mainContents); } @@ -106,13 +106,13 @@ describe('Init MFE', () => { 'should change the build and serve target and set correct path to webpack config', async (app, type: 'host' | 'remote') => { // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: app, mfeType: type, }); // ASSERT - const { build, serve } = readProjectConfiguration(host, app).targets; + const { build, serve } = readProjectConfiguration(tree, app).targets; expect(serve.executor).toEqual('@nrwl/angular:webpack-server'); expect(build.executor).toEqual('@nrwl/angular:webpack-browser'); @@ -124,20 +124,20 @@ describe('Init MFE', () => { it('should add the remote config to the host when --remotes flag supplied', async () => { // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', remotes: ['remote1'], }); // ASSERT - const mfeConfigContents = host.read(`apps/app1/mfe.config.js`, 'utf-8'); + const mfeConfigContents = tree.read(`apps/app1/mfe.config.js`, 'utf-8'); expect(mfeConfigContents).toContain(`'remote1'`); }); it('should update the implicit dependencies of the host when --remotes flag supplied', async () => { // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', remotes: ['remote1'], @@ -145,7 +145,7 @@ describe('Init MFE', () => { // ASSERT const projectConfig: ProjectConfiguration = readProjectConfiguration( - host, + tree, 'app1' ); @@ -154,35 +154,35 @@ describe('Init MFE', () => { 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, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', }); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote1', mfeType: 'remote', host: 'app1', }); // ASSERT - const hostMfeConfig = host.read('apps/app1/mfe.config.js', 'utf-8'); + const hostMfeConfig = tree.read('apps/app1/mfe.config.js', 'utf-8'); expect(hostMfeConfig).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, { + await applicationGenerator(tree, { name: 'remote2', }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote1', mfeType: 'remote', host: 'app1', @@ -190,7 +190,7 @@ describe('Init MFE', () => { }); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote2', mfeType: 'remote', host: 'app1', @@ -198,24 +198,24 @@ describe('Init MFE', () => { }); // ASSERT - const hostMfeConfig = host.read('apps/app1/mfe.config.js', 'utf-8'); + const hostMfeConfig = tree.read('apps/app1/mfe.config.js', 'utf-8'); expect(hostMfeConfig).toMatchSnapshot(); }); it('should add a remote application and add it to a specified host applications router config', async () => { // ARRANGE - await applicationGenerator(host, { + await applicationGenerator(tree, { name: 'remote2', routing: true, }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', routing: true, }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote1', mfeType: 'remote', host: 'app1', @@ -224,7 +224,7 @@ describe('Init MFE', () => { }); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote2', mfeType: 'remote', host: 'app1', @@ -233,24 +233,24 @@ describe('Init MFE', () => { }); // ASSERT - const hostAppModule = host.read('apps/app1/src/app/app.module.ts', 'utf-8'); + const hostAppModule = tree.read('apps/app1/src/app/app.module.ts', 'utf-8'); expect(hostAppModule).toMatchSnapshot(); }); it('should add a remote application and add it to a specified host applications serve-mfe target', async () => { // ARRANGE - await applicationGenerator(host, { + await applicationGenerator(tree, { name: 'remote2', routing: true, }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'app1', mfeType: 'host', routing: true, }); - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote1', mfeType: 'remote', host: 'app1', @@ -259,7 +259,7 @@ describe('Init MFE', () => { }); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'remote2', mfeType: 'remote', host: 'app1', @@ -268,7 +268,7 @@ describe('Init MFE', () => { }); // ASSERT - const hostAppConfig = readProjectConfiguration(host, 'app1'); + const hostAppConfig = readProjectConfiguration(tree, 'app1'); const serveMfe = hostAppConfig.targets['serve-mfe']; expect(serveMfe.options.commands).toContain('nx serve remote1'); @@ -278,20 +278,20 @@ describe('Init MFE', () => { it('should modify the associated cypress project to add the workaround correctly', async () => { // ARRANGE - await applicationGenerator(host, { + await applicationGenerator(tree, { name: 'testApp', routing: true, }); // ACT - await setupMfe(host, { + await setupMfe(tree, { appName: 'test-app', mfeType: 'host', routing: true, }); // ASSERT - const cypressCommands = host.read( + const cypressCommands = tree.read( 'apps/test-app-e2e/src/support/index.ts', 'utf-8' ); @@ -299,4 +299,55 @@ describe('Init MFE', () => { "Cannot use 'import.meta' outside a module" ); }); + + describe('--federationType=dynamic', () => { + it('should create a host with the correct configurations', async () => { + // ARRANGE & ACT + await setupMfe(tree, { + appName: 'app1', + mfeType: 'host', + routing: true, + federationType: 'dynamic', + }); + + // ASSERT + expect(tree.read('apps/app1/mfe.config.js', 'utf-8')).toContain( + 'remotes: []' + ); + expect( + tree.exists('apps/app1/src/assets/mfe.manifest.json') + ).toBeTruthy(); + expect(tree.read('apps/app1/src/main.ts', 'utf-8')).toMatchSnapshot(); + }); + }); + + it('should add a remote to dynamic host correctly', async () => { + // ARRANGE + await setupMfe(tree, { + appName: 'app1', + mfeType: 'host', + routing: true, + federationType: 'dynamic', + }); + + // ACT + await setupMfe(tree, { + appName: 'remote1', + mfeType: 'remote', + port: 4201, + host: 'app1', + routing: true, + }); + + // ASSERT + expect(tree.read('apps/app1/mfe.config.js', 'utf-8')).toContain( + 'remotes: []' + ); + expect(readJson(tree, 'apps/app1/src/assets/mfe.manifest.json')).toEqual({ + remote1: 'http://localhost:4201', + }); + expect( + tree.read('apps/app1/src/app/app.module.ts', 'utf-8') + ).toMatchSnapshot(); + }); }); diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.ts index 91095405f83ba..b2054874b4e89 100644 --- a/packages/angular/src/generators/setup-mfe/setup-mfe.ts +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.ts @@ -13,30 +13,35 @@ import { generateWebpackConfig, getRemotesWithPorts, setupServeTarget, + setupHostIfDynamic, updateTsConfigTarget, } from './lib'; -export async function setupMfe(host: Tree, options: Schema) { - const projectConfig = readProjectConfiguration(host, options.appName); +export async function setupMfe(tree: Tree, options: Schema) { + const projectConfig = readProjectConfiguration(tree, options.appName); - const remotesWithPorts = getRemotesWithPorts(host, options); - addRemoteToHost(host, options); + options.federationType = options.federationType ?? 'static'; - generateWebpackConfig(host, options, projectConfig.root, remotesWithPorts); + setupHostIfDynamic(tree, options); - addEntryModule(host, options, projectConfig.root); - addImplicitDeps(host, options); - changeBuildTarget(host, options); - updateTsConfigTarget(host, options); - setupServeTarget(host, options); + const remotesWithPorts = getRemotesWithPorts(tree, options); + addRemoteToHost(tree, options); - fixBootstrap(host, projectConfig.root); + generateWebpackConfig(tree, options, projectConfig.root, remotesWithPorts); - addCypressOnErrorWorkaround(host, options); + addEntryModule(tree, options, projectConfig.root); + addImplicitDeps(tree, options); + changeBuildTarget(tree, options); + updateTsConfigTarget(tree, options); + setupServeTarget(tree, options); + + fixBootstrap(tree, projectConfig.root, options); + + addCypressOnErrorWorkaround(tree, options); // format files if (!options.skipFormat) { - await formatFiles(host); + await formatFiles(tree); } }