diff --git a/docs/angular/api-angular/generators/setup-mfe.md b/docs/angular/api-angular/generators/setup-mfe.md new file mode 100644 index 0000000000..01cf0f5750 --- /dev/null +++ b/docs/angular/api-angular/generators/setup-mfe.md @@ -0,0 +1,59 @@ +# setup-mfe + +Generate a Module Federation configuration for a given Angular application. + +## Usage + +```bash +nx generate setup-mfe ... +``` + +By default, Nx will search for `setup-mfe` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:setup-mfe ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g setup-mfe ... --dry-run +``` + +## Options + +### appName + +Type: `string` + +The name of the application to generate the Module Federation configuration for. + +### mfeType + +Default: `shell` + +Type: `string` + +Possible values: `shell`, `remote` + +Type of application to generate the Module Federation configuration for. + +### port + +Type: `number` + +The port at which the remote application should be served. + +### remotes + +Type: `array` + +A list of remote application names that the shell application should consume. + +### skipFormat + +Type: `boolean` + +Skip formatting the workspace after the generator completes. diff --git a/docs/angular/api-nx-devkit/index.md b/docs/angular/api-nx-devkit/index.md index 1e71e514da..ee030b2a23 100644 --- a/docs/angular/api-nx-devkit/index.md +++ b/docs/angular/api-nx-devkit/index.md @@ -1031,6 +1031,8 @@ Reads a project configuration. The project configuration is stored in workspace.json and nx.json. The utility will read both files. +**`throws`** If supplied projectName cannot be found + #### Parameters | Name | Type | Description | diff --git a/docs/map.json b/docs/map.json index bf63d4cb52..4d7ed05b40 100644 --- a/docs/map.json +++ b/docs/map.json @@ -422,6 +422,11 @@ "id": "ngrx", "file": "angular/api-angular/generators/ngrx" }, + { + "name": "setup-mfe generator", + "id": "setup-mfe", + "file": "angular/api-angular/generators/setup-mfe" + }, { "name": "stories generator", "id": "stories", @@ -1558,6 +1563,11 @@ "id": "ngrx", "file": "react/api-angular/generators/ngrx" }, + { + "name": "setup-mfe generator", + "id": "setup-mfe", + "file": "react/api-angular/generators/setup-mfe" + }, { "name": "stories generator", "id": "stories", @@ -2658,6 +2668,11 @@ "id": "ngrx", "file": "node/api-angular/generators/ngrx" }, + { + "name": "setup-mfe generator", + "id": "setup-mfe", + "file": "node/api-angular/generators/setup-mfe" + }, { "name": "stories generator", "id": "stories", diff --git a/docs/node/api-angular/generators/setup-mfe.md b/docs/node/api-angular/generators/setup-mfe.md new file mode 100644 index 0000000000..3f085f3153 --- /dev/null +++ b/docs/node/api-angular/generators/setup-mfe.md @@ -0,0 +1,59 @@ +# setup-mfe + +Generate a Module Federation configuration for a given Angular application. + +## Usage + +```bash +nx generate setup-mfe ... +``` + +By default, Nx will search for `setup-mfe` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:setup-mfe ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g setup-mfe ... --dry-run +``` + +## Options + +### appName + +Type: `string` + +The name of the application to generate the Module Federation configuration for. + +### mfeType + +Default: `shell` + +Type: `string` + +Possible values: `shell`, `remote` + +Type of application to generate the Module Federation configuration for. + +### port + +Type: `number` + +The port at which the remote application should be served. + +### remotes + +Type: `array` + +A list of remote application names that the shell application should consume. + +### skipFormat + +Type: `boolean` + +Skip formatting the workspace after the generator completes. diff --git a/docs/node/api-nx-devkit/index.md b/docs/node/api-nx-devkit/index.md index 3801edbb56..29de309e8b 100644 --- a/docs/node/api-nx-devkit/index.md +++ b/docs/node/api-nx-devkit/index.md @@ -1031,6 +1031,8 @@ Reads a project configuration. The project configuration is stored in workspace.json and nx.json. The utility will read both files. +**`throws`** If supplied projectName cannot be found + #### Parameters | Name | Type | Description | diff --git a/docs/react/api-angular/generators/setup-mfe.md b/docs/react/api-angular/generators/setup-mfe.md new file mode 100644 index 0000000000..3f085f3153 --- /dev/null +++ b/docs/react/api-angular/generators/setup-mfe.md @@ -0,0 +1,59 @@ +# setup-mfe + +Generate a Module Federation configuration for a given Angular application. + +## Usage + +```bash +nx generate setup-mfe ... +``` + +By default, Nx will search for `setup-mfe` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:setup-mfe ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g setup-mfe ... --dry-run +``` + +## Options + +### appName + +Type: `string` + +The name of the application to generate the Module Federation configuration for. + +### mfeType + +Default: `shell` + +Type: `string` + +Possible values: `shell`, `remote` + +Type of application to generate the Module Federation configuration for. + +### port + +Type: `number` + +The port at which the remote application should be served. + +### remotes + +Type: `array` + +A list of remote application names that the shell application should consume. + +### skipFormat + +Type: `boolean` + +Skip formatting the workspace after the generator completes. diff --git a/docs/react/api-nx-devkit/index.md b/docs/react/api-nx-devkit/index.md index b18093175e..0256d7d9f3 100644 --- a/docs/react/api-nx-devkit/index.md +++ b/docs/react/api-nx-devkit/index.md @@ -1031,6 +1031,8 @@ Reads a project configuration. The project configuration is stored in workspace.json and nx.json. The utility will read both files. +**`throws`** If supplied projectName cannot be found + #### Parameters | Name | Type | Description | diff --git a/docs/shared/angular-plugin.md b/docs/shared/angular-plugin.md index 5e9dffe9d3..0d21e37cd5 100644 --- a/docs/shared/angular-plugin.md +++ b/docs/shared/angular-plugin.md @@ -122,6 +122,7 @@ myorg/ - [library](/{{framework}}/angular/library) - Creates an Angular library. - [move](/{{framework}}/angular/move) - Moves an Angular application or library to another folder within the workspace and updates the project configuration. - [ngrx](/{{framework}}/angular/ngrx) - Adds NgRx support to an application or library. +- [setup-mfe](/{{framework}}/angular/setup-mfe) - Generate a Module Federation configuration for a given Angular application. - [stories](/{{framework}}/angular/stories) - Creates stories/specs for all components declared in a project. - [storybook-configuration](/{{framework}}/angular/storybook-configuration) - Adds Storybook configuration to a project. - [storybook-migrate-defaults-5-to-6](/{{framework}}/angular/storybook-migrate-defaults-5-to-6) - Generates default Storybook configuration files using Storybook version >=6.x specs, for projects that already have Storybook instances and configurations of versions <6.x. diff --git a/package.json b/package.json index 9738068ebb..e46446ba8d 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "@storybook/angular": "~6.3.0", "@storybook/core": "~6.3.0", "@storybook/react": "~6.3.0", - "@svgr/webpack": "^5.4.0", "@svgr/webpack": "5.5.0", "@tailwindcss/typography": "^0.4.0", "@testing-library/react": "11.2.5", @@ -279,4 +278,4 @@ "ng-packagr/rxjs": "6.6.7", "**/xmlhttprequest-ssl": "~1.6.2" } -} +} \ No newline at end of file diff --git a/packages/angular/collection.json b/packages/angular/collection.json index 4f36fd76a1..5ab79798b0 100644 --- a/packages/angular/collection.json +++ b/packages/angular/collection.json @@ -96,6 +96,13 @@ "schema": "./src/generators/upgrade-module/schema.json", "description": "Sets up an Upgrade Module." }, + + "setup-mfe": { + "factory": "./src/generators/setup-mfe/compat", + "schema": "./src/generators/setup-mfe/schema.json", + "description": "Generate a Module Federation configuration for a given Angular application." + }, + "web-worker": { "factory": "./src/generators/web-worker/compat", "schema": "./src/generators/web-worker/schema.json", @@ -115,6 +122,11 @@ "aliases": ["app"], "description": "Creates an Angular application." }, + "setup-mfe": { + "factory": "./src/generators/setup-mfe/setup-mfe", + "schema": "./src/generators/setup-mfe/schema.json", + "description": "Generate a Module Federation configuration for a given Angular application." + }, "component-cypress-spec": { "factory": "./src/generators/component-cypress-spec/component-cypress-spec", "schema": "./src/generators/component-cypress-spec/schema.json", diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index 18be438711..2b30b5aa1c 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -11,3 +11,4 @@ export * from './src/generators/storybook-configuration/storybook-configuration' export * from './src/generators/storybook-migrate-defaults-5-to-6/storybook-migrate-defaults-5-to-6'; 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'; 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 new file mode 100644 index 0000000000..6e45681f51 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/__snapshots__/setup-mfe.spec.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Init MFE should create webpack configs correctly 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: { + + }, + 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 2`] = ` +"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: \\"remote1\\", + publicPath: \\"auto\\", + }, + optimization: { + runtimeChunk: false, + minimize: false, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + name: \\"remote1\\", + filename: \\"remoteEntry.js\\", + exposes: { + './Component': 'apps/remote1/src/app/app.component.ts', + }, + 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(), + ], +}; +" +`; diff --git a/packages/angular/src/generators/setup-mfe/files/webpack.config.js__tmpl__ b/packages/angular/src/generators/setup-mfe/files/webpack.config.js__tmpl__ new file mode 100644 index 0000000000..4ee66a85fc --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/files/webpack.config.js__tmpl__ @@ -0,0 +1,41 @@ +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: "<%= name %>", + publicPath: "auto", + }, + optimization: { + runtimeChunk: false, + minimize: false, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({<% if(type === 'remote') { %> + name: "<%= name %>", + filename: "remoteEntry.js", + exposes: { + './Component': '<%= sourceRoot %>/src/app/app.component.ts', + },<% } %><% if(type === 'shell') { %> + remotes: { + <% remotes.forEach(function(remote) { %>"<%= remote.remoteName %>": "<%= remote.remoteName %>@http://localhost:<%= remote.port %>/remoteEntry.js",<% }); %> + },<% } %> + shared: {<% sharedLibraries.forEach(function (lib) { %> + "<%= lib %>": { singleton: true, strictVersion: true },<% }); %> + ...sharedMappings.getDescriptors(), + }, + }), + sharedMappings.getPlugin(), + ], +}; diff --git a/packages/angular/src/generators/setup-mfe/files/webpack.prod.config.js__tmpl__ b/packages/angular/src/generators/setup-mfe/files/webpack.prod.config.js__tmpl__ new file mode 100644 index 0000000000..bbf8e1f9d4 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/files/webpack.prod.config.js__tmpl__ @@ -0,0 +1 @@ +module.exports = require('./webpack.config'); \ No newline at end of file diff --git a/packages/angular/src/generators/setup-mfe/lib/add-implicit-deps.ts b/packages/angular/src/generators/setup-mfe/lib/add-implicit-deps.ts new file mode 100644 index 0000000000..298ae8d89a --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/add-implicit-deps.ts @@ -0,0 +1,23 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { + readProjectConfiguration, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +export function addImplicitDeps(host: Tree, options: Schema) { + if ( + options.mfeType === 'shell' && + Array.isArray(options.remotes) && + options.remotes.length > 0 + ) { + const appConfig = readProjectConfiguration(host, options.appName); + appConfig.implicitDependencies = Array.isArray( + appConfig.implicitDependencies + ) + ? [...appConfig.implicitDependencies, ...options.remotes] + : [...options.remotes]; + updateProjectConfiguration(host, options.appName, appConfig); + } +} diff --git a/packages/angular/src/generators/setup-mfe/lib/change-build-target.ts b/packages/angular/src/generators/setup-mfe/lib/change-build-target.ts new file mode 100644 index 0000000000..31ec079954 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/change-build-target.ts @@ -0,0 +1,28 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { + readProjectConfiguration, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +export function changeBuildTarget(host: Tree, options: Schema) { + const appConfig = readProjectConfiguration(host, options.appName); + + appConfig.targets.build.executor = '@nrwl/angular:webpack-browser'; + appConfig.targets.build.options = { + ...appConfig.targets.build.options, + customWebpackConfig: { + path: `${appConfig.root}/webpack.config.js`, + }, + }; + + appConfig.targets.build.configurations.production = { + ...appConfig.targets.build.configurations.production, + customWebpackConfig: { + path: `${appConfig.root}/webpack.prod.config.js`, + }, + }; + + updateProjectConfiguration(host, options.appName, appConfig); +} diff --git a/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts b/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts new file mode 100644 index 0000000000..76a71b043f --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/fix-bootstrap.ts @@ -0,0 +1,15 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { joinPathFragments } from '@nrwl/devkit'; + +export function fixBootstrap(host: Tree, appRoot: string) { + const mainFilePath = joinPathFragments(appRoot, 'src/main.ts'); + const bootstrapCode = host.read(mainFilePath, 'utf-8'); + host.write(joinPathFragments(appRoot, 'src/bootstrap.ts'), bootstrapCode); + + host.write( + mainFilePath, + `import('./bootstrap').catch(err => console.error(err));` + ); +} diff --git a/packages/angular/src/generators/setup-mfe/lib/generate-config.ts b/packages/angular/src/generators/setup-mfe/lib/generate-config.ts new file mode 100644 index 0000000000..f57bdf4d2e --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/generate-config.ts @@ -0,0 +1,35 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; +import { generateFiles, joinPathFragments, logger } from '@nrwl/devkit'; + +const SHARED_SINGLETON_LIBRARIES = [ + '@angular/core', + '@angular/common', + '@angular/common/http', + '@angular/router', +]; + +export function generateWebpackConfig( + host: Tree, + options: Schema, + appRoot: string, + remotesWithPorts: { remoteName: string; port: number }[] +) { + if ( + host.exists(`${appRoot}/webpack.config.js`) || + host.exists(`${appRoot}/webpack.prod.config.js`) + ) { + logger.warn( + `NOTE: We encountered an existing webpack config for the app ${options.appName}. We have overwritten this file with the Module Federation Config.\n + If this was not the outcome you expected, you can discard the changes we have made, create a backup of your current webpack config, and run the command again.` + ); + } + generateFiles(host, joinPathFragments(__dirname, '../files'), appRoot, { + tmpl: '', + type: options.mfeType, + name: options.appName, + remotes: remotesWithPorts ?? [], + sourceRoot: appRoot, + sharedLibraries: SHARED_SINGLETON_LIBRARIES, + }); +} 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 new file mode 100644 index 0000000000..0fb61a79bf --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/get-remotes-with-ports.ts @@ -0,0 +1,23 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { readProjectConfiguration } from '@nrwl/devkit'; + +export function getRemotesWithPorts(host: Tree, options: Schema) { + // If type is shell and remotes supplied, check remotes exist + const remotesWithPort: { remoteName: string; port: number }[] = []; + if ( + options.mfeType === 'shell' && + Array.isArray(options.remotes) && + options.remotes.length > 0 + ) { + for (const remote of options.remotes) { + const remoteConfig = readProjectConfiguration(host, remote); + remotesWithPort.push({ + remoteName: remote, + port: remoteConfig.targets['mfe-serve']?.options.port ?? 4200, + }); + } + } + return remotesWithPort; +} diff --git a/packages/angular/src/generators/setup-mfe/lib/index.ts b/packages/angular/src/generators/setup-mfe/lib/index.ts new file mode 100644 index 0000000000..b9a5b6adb7 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/index.ts @@ -0,0 +1,6 @@ +export * from './add-implicit-deps'; +export * from './change-build-target'; +export * from './fix-bootstrap'; +export * from './generate-config'; +export * from './get-remotes-with-ports'; +export * from './setup-serve-target'; diff --git a/packages/angular/src/generators/setup-mfe/lib/setup-serve-target.ts b/packages/angular/src/generators/setup-mfe/lib/setup-serve-target.ts new file mode 100644 index 0000000000..b52d7f8334 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/lib/setup-serve-target.ts @@ -0,0 +1,25 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from '../schema'; + +import { + readProjectConfiguration, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +export function setupServeTarget(host: Tree, options: Schema) { + if (options.mfeType === 'remote') { + const appConfig = readProjectConfiguration(host, options.appName); + + const port = options.port ?? 4200; + + appConfig.targets['mfe-serve'] = { + executor: '@nrwl/workspace:run-commands', + options: { + command: `nx serve ${options.appName}`, + port, + }, + }; + + updateProjectConfiguration(host, options.appName, appConfig); + } +} diff --git a/packages/angular/src/generators/setup-mfe/schema.d.ts b/packages/angular/src/generators/setup-mfe/schema.d.ts new file mode 100644 index 0000000000..3202afff3d --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/schema.d.ts @@ -0,0 +1,7 @@ +export interface Schema { + appName: string; + mfeType: 'shell' | 'remote'; + port?: number; + remotes?: string[]; + skipFormat?: boolean; +} diff --git a/packages/angular/src/generators/setup-mfe/schema.json b/packages/angular/src/generators/setup-mfe/schema.json new file mode 100644 index 0000000000..567b943cf1 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "GeneratorAngularMFESetup", + "cli": "nx", + "title": "Generate Module Federation Setup for Angular App", + "description": "Create Module Federation configuration files for given Angular Application.", + "type": "object", + "properties": { + "appName": { + "type": "string", + "description": "The name of the application to generate the Module Federation configuration for.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What app would you like to generate a Module Federation configuration for?" + }, + "mfeType": { + "type": "string", + "enum": ["shell", "remote"], + "description": "Type of application to generate the Module Federation configuration for.", + "default": "shell" + }, + "port": { + "type": "number", + "description": "The port at which the remote application should be served." + }, + "remotes": { + "type": "array", + "description": "A list of remote application names that the shell application should consume." + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting the workspace after the generator completes." + } + }, + "required": ["appName", "mfeType"], + "additionalProperties": false +} diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.compat.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.compat.ts new file mode 100644 index 0000000000..42437a0cd1 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import { setupMfe } from './setup-mfe'; + +export default convertNxGenerator(setupMfe); diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts new file mode 100644 index 0000000000..06575c6797 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.spec.ts @@ -0,0 +1,167 @@ +import type { NxJsonConfiguration, Tree } from '@nrwl/devkit'; +import { readJson, 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; + + beforeEach(async () => { + host = createTreeWithEmptyWorkspace(); + await applicationGenerator(host, { + name: 'app1', + }); + await applicationGenerator(host, { + name: 'remote1', + }); + }); + + test.each([ + ['app1', 'shell'], + ['remote1', 'remote'], + ])( + 'should create webpack configs correctly', + async (app, type: 'shell' | 'remote') => { + // ACT + await setupMfe(host, { + appName: app, + mfeType: type, + }); + + // ASSERT + expect(host.exists(`apps/${app}/webpack.config.js`)).toBeTruthy(); + expect(host.exists(`apps/${app}/webpack.prod.config.js`)).toBeTruthy(); + + const webpackContetnts = host.read( + `apps/${app}/webpack.config.js`, + 'utf-8' + ); + expect(webpackContetnts).toMatchSnapshot(); + } + ); + + test.each([ + ['app1', 'shell'], + ['remote1', 'remote'], + ])( + 'create bootstrap file with the contents of main.ts', + async (app, type: 'shell' | 'remote') => { + // ARRANGE + const mainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + + // ACT + await setupMfe(host, { + appName: app, + mfeType: type, + }); + + // ASSERT + const bootstrapContents = host.read( + `apps/${app}/src/bootstrap.ts`, + 'utf-8' + ); + const updatedMainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + + expect(bootstrapContents).toEqual(mainContents); + expect(updatedMainContents).not.toEqual(mainContents); + } + ); + + test.each([ + ['app1', 'shell'], + ['remote1', 'remote'], + ])( + 'should alter main.ts to import the bootstrap file dynamically', + async (app, type: 'shell' | 'remote') => { + // ARRANGE + const mainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + + // ACT + await setupMfe(host, { + appName: app, + mfeType: type, + }); + + // ASSERT + const updatedMainContents = host.read(`apps/${app}/src/main.ts`, 'utf-8'); + + expect(updatedMainContents).toEqual( + `import('./bootstrap').catch(err => console.error(err));` + ); + expect(updatedMainContents).not.toEqual(mainContents); + } + ); + + test.each([ + ['app1', 'shell'], + ['remote1', 'remote'], + ])( + 'should change the build target and set correct path to webpack config', + async (app, type: 'shell' | 'remote') => { + // ACT + await setupMfe(host, { + appName: app, + mfeType: type, + }); + + // ASSERT + const { build } = readProjectConfiguration(host, app).targets; + + expect(build.executor).toEqual('@nrwl/angular:webpack-browser'); + expect(build.options.customWebpackConfig.path).toEqual( + `apps/${app}/webpack.config.js` + ); + } + ); + + test.each([ + ['app1', 'shell'], + ['remote1', 'remote'], + ])( + 'should install @angular-architects/module-federation in the monorepo', + async (app, type: 'shell' | 'remote') => { + // ACT + await setupMfe(host, { + appName: app, + mfeType: type, + }); + + // ASSERT + const { dependencies } = readJson(host, 'package.json'); + + expect( + dependencies['@angular-architects/module-federation'] + ).toBeTruthy(); + } + ); + + it('should add the remote config to the shell when --remotes flag supplied', async () => { + // ACT + await setupMfe(host, { + appName: 'app1', + mfeType: 'shell', + remotes: ['remote1'], + }); + + // ASSERT + const webpackContents = host.read(`apps/app1/webpack.config.js`, 'utf-8'); + + expect(webpackContents).toContain( + '"remote1": "remote1@http://localhost:4200/remoteEntry.js"' + ); + }); + it('should update the implicit dependencies of the shell when --remotes flag supplied', async () => { + // ACT + await setupMfe(host, { + appName: 'app1', + mfeType: 'shell', + remotes: ['remote1'], + }); + + // ASSERT + const nxJson: NxJsonConfiguration = readJson(host, 'nx.json'); + + expect(nxJson.projects['app1'].implicitDependencies).toContain('remote1'); + }); +}); diff --git a/packages/angular/src/generators/setup-mfe/setup-mfe.ts b/packages/angular/src/generators/setup-mfe/setup-mfe.ts new file mode 100644 index 0000000000..b20c12c6c5 --- /dev/null +++ b/packages/angular/src/generators/setup-mfe/setup-mfe.ts @@ -0,0 +1,47 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from './schema'; + +import { + readProjectConfiguration, + addDependenciesToPackageJson, + formatFiles, +} from '@nrwl/devkit'; + +import { + addImplicitDeps, + changeBuildTarget, + fixBootstrap, + generateWebpackConfig, + getRemotesWithPorts, + setupServeTarget, +} from './lib'; + +export async function setupMfe(host: Tree, options: Schema) { + const projectConfig = readProjectConfiguration(host, options.appName); + + const remotesWithPorts = getRemotesWithPorts(host, options); + + generateWebpackConfig(host, options, projectConfig.root, remotesWithPorts); + + addImplicitDeps(host, options); + changeBuildTarget(host, options); + setupServeTarget(host, options); + + fixBootstrap(host, projectConfig.root); + + // add package to install + const installPackages = addDependenciesToPackageJson( + host, + { '@angular-architects/module-federation': '^12.2.0' }, + {} + ); + + // format files + if (!options.skipFormat) { + await formatFiles(host); + } + + return installPackages; +} + +export default setupMfe; diff --git a/packages/devkit/src/generators/project-configuration.ts b/packages/devkit/src/generators/project-configuration.ts index 70fbe5d025..e7bc1862ab 100644 --- a/packages/devkit/src/generators/project-configuration.ts +++ b/packages/devkit/src/generators/project-configuration.ts @@ -151,6 +151,7 @@ export function updateWorkspaceConfiguration( * * @param host - the file system tree * @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib) + * @throws If supplied projectName cannot be found */ export function readProjectConfiguration( host: Tree,