diff --git a/docs/generated/api-angular/generators/move-to-with-mf.md b/docs/generated/api-angular/generators/move-to-with-mf.md new file mode 100644 index 00000000000000..cefdb18afecf3d --- /dev/null +++ b/docs/generated/api-angular/generators/move-to-with-mf.md @@ -0,0 +1,36 @@ +--- +title: '@nrwl/angular:move-to-with-mf generator' +description: 'Moves an old micro frontend configuration to use withModuleFederation helper.' +--- + +# @nrwl/angular:move-to-with-mf + +Moves an old micro frontend configuration to use withModuleFederation helper. + +## Usage + +```bash +nx generate move-to-with-mf ... +``` + +By default, Nx will search for `move-to-with-mf` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:move-to-with-mf ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g move-to-with-mf ... --dry-run +``` + +## Options + +### project + +Type: `string` + +The name of the micro frontend project to migrate. diff --git a/docs/map.json b/docs/map.json index 0e893fde01e6a2..29e180ae3e45fb 100644 --- a/docs/map.json +++ b/docs/map.json @@ -689,6 +689,11 @@ "id": "move", "file": "generated/api-angular/generators/move" }, + { + "name": "move-to-with-mf generator", + "id": "move-to-with-mf", + "file": "generated/api-angular/generators/move-to-with-mf" + }, { "name": "ngrx generator", "id": "ngrx", diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 57df82b7a1a36c..c3ded69cd07459 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -80,6 +80,11 @@ "aliases": ["mv"], "description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration." }, + "move-to-with-mf": { + "factory": "./src/generators/move-to-with-mf/move-to-with-mf.compat", + "schema": "./src/generators/move-to-with-mf/schema.json", + "description": "Moves an old micro frontend configuration to use withModuleFederation helper." + }, "mfe-host": { "factory": "./src/generators/mfe-host/mfe-host.compat", "schema": "./src/generators/mfe-host/schema.json", @@ -225,6 +230,11 @@ "aliases": ["mv"], "description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration." }, + "move-to-with-mf": { + "factory": "./src/generators/move-to-with-mf/move-to-with-mf", + "schema": "./src/generators/move-to-with-mf/schema.json", + "description": "Moves an old micro frontend configuration to use withModuleFederation helper." + }, "mfe-host": { "factory": "./src/generators/mfe-host/mfe-host", "schema": "./src/generators/mfe-host/schema.json", diff --git a/packages/angular/src/generators/move-to-with-mf/__snapshots__/move-to-with-mf.spec.ts.snap b/packages/angular/src/generators/move-to-with-mf/__snapshots__/move-to-with-mf.spec.ts.snap new file mode 100644 index 00000000000000..2b2ac1984aad5b --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/__snapshots__/move-to-with-mf.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`moveToWithMF should migrate a standard previous generated host config correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'host1', + remotes: [['remote1', 'http://localhost:4201']], + });" +`; + +exports[`moveToWithMF should migrate a standard previous generated remote config correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'remote1', + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts', + }, + });" +`; + +exports[`moveToWithMF should migrate a standard previous generated remote config using object shared syntax correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'remote1', + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts', + }, + });" +`; diff --git a/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/is-host-remote-config.spec.ts.snap b/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/is-host-remote-config.spec.ts.snap new file mode 100644 index 00000000000000..26ea2e502efbba --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/is-host-remote-config.spec.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`isHostRemoteConfig should return remote when correct remote config found 1`] = ` +"{ + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts' + }" +`; diff --git a/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/write-new-webpack-config.spec.ts.snap b/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/write-new-webpack-config.spec.ts.snap new file mode 100644 index 00000000000000..889987fc91bb6d --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/__snapshots__/write-new-webpack-config.spec.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`writeNewWebpackConfig should convert config that is both remote and host correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'both1', + remotes: [['remote1', 'http://localhost:4201']], + exposes: { + './Module': 'apps/both/src/app/remote-entry/entry.module.ts' + }, + });" +`; + +exports[`writeNewWebpackConfig should convert config that is neither remote and host correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'neither', + });" +`; + +exports[`writeNewWebpackConfig should convert host config correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'host1', + remotes: [['remote1', 'http://localhost:4201']], + });" +`; + +exports[`writeNewWebpackConfig should convert remote config correctly 1`] = ` +"const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: 'remote1', + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts' + }, + });" +`; diff --git a/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.spec.ts b/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.spec.ts new file mode 100644 index 00000000000000..0de7043ffd9771 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.spec.ts @@ -0,0 +1,37 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; +import { checkOutputNameMatchesProjectName } from './check-name-matches'; +describe('checkOutputNameMatchesProjectName', () => { + it('should return true if the uniqueName matches the project name', () => { + // ARRANGE + const sourceText = `module.exports = { + output: { + uniqueName: 'proj' + } + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkOutputNameMatchesProjectName(ast, 'proj'); + + // ASSERT + expect(result).toBeTruthy(); + }); + + it('should return false if the uniqueName does not match the project name', () => { + // ARRANGE + const sourceText = `module.exports = { + output: { + uniqueName: 'app1' + } + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkOutputNameMatchesProjectName(ast, 'proj'); + + // ASSERT + expect(result).toBeFalsy(); + }); +}); diff --git a/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.ts b/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.ts new file mode 100644 index 00000000000000..94efb92c58bc4f --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/check-name-matches.ts @@ -0,0 +1,29 @@ +import type { SourceFile } from 'typescript'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function checkOutputNameMatchesProjectName( + ast: SourceFile, + projectName: string +) { + const OUTPUT_SELECTOR = + 'PropertyAssignment:has(Identifier[name=output]) > ObjectLiteralExpression:has(PropertyAssignment:has(Identifier[name=uniqueName]))'; + const UNIQUENAME_SELECTOR = + 'ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=uniqueName]) > StringLiteral'; + + const outputNodes = tsquery(ast, OUTPUT_SELECTOR, { visitAllChildren: true }); + if (outputNodes.length === 0) { + // If the output isnt set in the config, then we can still set the project name correctly + return true; + } + + const uniqueNameNodes = tsquery(outputNodes[0], UNIQUENAME_SELECTOR, { + visitAllChildren: true, + }); + if (uniqueNameNodes.length === 0) { + // If the uniqeName isnt set in the config, then we can still set the project name correctly + return true; + } + const uniqueName = uniqueNameNodes[0].getText().replace(/'/g, ''); + + return uniqueName === projectName; +} diff --git a/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.spec.ts b/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.spec.ts new file mode 100644 index 00000000000000..ebb028fdb319d9 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.spec.ts @@ -0,0 +1,182 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; +import { checkSharedNpmPackagesMatchExpected } from './check-shared-npm-packages'; + +describe('checkSharedNpmPackagesMatchExpected', () => { + it('should return true if all shared packages match the config object we expect', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: share({ + '@angular/core': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true + }, + '@angular/common': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true + }, + 'rxjs': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true + }, + }) + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeTruthy(); + }); + + it('should return true if all shared packages match the config object we expect using object syntax', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: { + '@angular/core': { + singleton: true, + strictVersion: true, + }, + '@angular/common': { + singleton: true, + strictVersion: true, + }, + 'rxjs': { + singleton: true, + strictVersion: true, + }, + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeTruthy(); + }); + + it('should return false if any shared packages do not match the config object we expect using object syntax', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: { + '@angular/core': { + singleton: true, + strictVersion: true, + }, + '@angular/common': { + singleton: true, + strictVersion: false, + }, + 'rxjs': { + singleton: true, + strictVersion: true, + }, + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeFalsy(); + }); + + it('should return true if we arent sharing packages with the share helper', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: share({}) + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeTruthy(); + }); + + it('should return true if we arent sharing packages with the standard shared syntax', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: {} + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeTruthy(); + }); + + it('should return false if any shared packages does not match the config object we expect', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + shared: share({ + '@angular/core': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true + }, + '@angular/common': { + singleton: true, + strictVersion: false, + requiredVersion: 'auto', + includeSecondaries: true + }, + 'rxjs': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true + }, + }) + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = checkSharedNpmPackagesMatchExpected(ast); + + // ASSERT + expect(result).toBeFalsy(); + }); +}); diff --git a/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.ts b/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.ts new file mode 100644 index 00000000000000..85f57d5f73c468 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/check-shared-npm-packages.ts @@ -0,0 +1,68 @@ +import type { SourceFile, Node } from 'typescript'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function checkSharedNpmPackagesMatchExpected(ast: SourceFile) { + const SHARE_HELPER_SELECTOR = + 'PropertyAssignment:has(Identifier[name=shared]) > CallExpression:has(Identifier[name=share])'; + const SHARED_PACKAGE_CONFIG_SELECTOR = + 'ObjectLiteralExpression > PropertyAssignment > ObjectLiteralExpression'; + + const shareHelperNodes = tsquery(ast, SHARE_HELPER_SELECTOR, { + visitAllChildren: true, + }); + + let sharedPackageConfigNodes: Node[]; + let settingsToMatch: string[] = []; + if (shareHelperNodes.length === 0) { + // if we arent sharing using share helper, check for standard object sharing syntax + const SHARED_OBJECT_SELECTOR = + 'PropertyAssignment:has(Identifier[name=shared]) > ObjectLiteralExpression'; + const sharedObjectNodes = tsquery(ast, SHARED_OBJECT_SELECTOR, { + visitAllChildren: true, + }); + + if (sharedObjectNodes.length === 0) { + // nothing is being shared, we're safe to continue + return true; + } + + sharedPackageConfigNodes = tsquery( + sharedObjectNodes[0], + SHARED_PACKAGE_CONFIG_SELECTOR, + { visitAllChildren: true } + ); + + settingsToMatch = [`singleton: true`, `strictVersion: true`]; + } else { + sharedPackageConfigNodes = tsquery( + shareHelperNodes[0], + SHARED_PACKAGE_CONFIG_SELECTOR, + { visitAllChildren: true } + ); + + settingsToMatch = [ + `singleton: true`, + `strictVersion: true`, + `requiredVersion: 'auto'`, + ]; + } + + if (sharedPackageConfigNodes.length === 0) { + // we arent sharing configs with the share helper, so we can safely continue + return true; + } + + let packagesMatch = true; + for (const configNode of sharedPackageConfigNodes) { + const configText = configNode.getText(); + packagesMatch = settingsToMatch.every((setting) => + configText.includes(setting) + ); + + if (!packagesMatch) { + break; + } + } + + return packagesMatch; +} diff --git a/packages/angular/src/generators/move-to-with-mf/lib/get-webpack-config-path.ts b/packages/angular/src/generators/move-to-with-mf/lib/get-webpack-config-path.ts new file mode 100644 index 00000000000000..680301a7e32a2f --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/get-webpack-config-path.ts @@ -0,0 +1,25 @@ +import type { ProjectConfiguration } from '@nrwl/devkit'; + +export function getWebpackConfigPath( + project: ProjectConfiguration, + projectName: string +) { + let pathToWebpackConfig = ''; + for (const target of Object.values(project.targets)) { + if ( + target.executor === '@nrwl/angular:webpack-browser' && + target.options.customWebpackConfig?.path + ) { + pathToWebpackConfig = target.options.customWebpackConfig?.path; + break; + } + } + + if (!pathToWebpackConfig) { + throw new Error( + `Could not find webpack config for \`${projectName}\` in your workspace.` + ); + } + + return pathToWebpackConfig; +} diff --git a/packages/angular/src/generators/move-to-with-mf/lib/index.ts b/packages/angular/src/generators/move-to-with-mf/lib/index.ts new file mode 100644 index 00000000000000..f1da8f1bdf7b64 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/index.ts @@ -0,0 +1,6 @@ +export * from './check-name-matches'; +export * from './check-shared-npm-packages'; +export * from './get-webpack-config-path'; +export * from './is-host-remote-config'; +export * from './parse-ast-webpack-config'; +export * from './write-new-webpack-config'; diff --git a/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.spec.ts b/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.spec.ts new file mode 100644 index 00000000000000..b201cc2fffe927 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.spec.ts @@ -0,0 +1,135 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; +import { + isHostRemoteConfig, + getRemotesFromHost, + getExposedModulesFromRemote, +} from './is-host-remote-config'; + +describe('isHostRemoteConfig', () => { + it('should return host when correct host config found', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = isHostRemoteConfig(ast); + + // ASSERT + expect(result).toEqual('host'); + }); + + it('should return remote when correct remote config found', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = isHostRemoteConfig(ast); + + // ASSERT + expect(result).toEqual('remote'); + }); + + it('should return both when correct remote and host config found', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs' + }, + exposes: { + './Module': 'apps/both/src/app/remote-entry/entry.module.ts' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = isHostRemoteConfig(ast); + + // ASSERT + expect(result).toEqual('both'); + }); + + it('should return false when no valid config found', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = isHostRemoteConfig(ast); + + // ASSERT + expect(result).toBeFalsy(); + }); + + it('should return remotes from the host correctly', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = getRemotesFromHost(ast); + + // ASSERT + expect(result).toEqual([['remote1', 'http://localhost:4201']]); + }); + + it('should return remote when correct remote config found', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = getExposedModulesFromRemote(ast); + + // ASSERT + // this needs to be snapshot because prettier formats a literal string incorrectly, causing test failure + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.ts b/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.ts new file mode 100644 index 00000000000000..e550c7f4576a5b --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/is-host-remote-config.ts @@ -0,0 +1,73 @@ +import type { SourceFile } from 'typescript'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export type IsHostRemoteConfigResult = 'host' | 'remote' | 'both' | false; + +const REMOTES_EXPRESSION_SELECTOR = + 'PropertyAssignment:has(Identifier[name=remotes]) > ObjectLiteralExpression'; +const EXPOSES_EXPRESSION_SELECTOR = + 'PropertyAssignment:has(Identifier[name=exposes]) > ObjectLiteralExpression'; +const PROPERTY_SELECTOR = 'ObjectLiteralExpression > PropertyAssignment'; + +export function isHostRemoteConfig(ast: SourceFile): IsHostRemoteConfigResult { + let isHost = false; + let isRemote = false; + + const remotesNodes = tsquery(ast, REMOTES_EXPRESSION_SELECTOR, { + visitAllChildren: true, + }); + if (remotesNodes.length > 0) { + isHost = true; + } + + const exposesNodes = tsquery(ast, EXPOSES_EXPRESSION_SELECTOR, { + visitAllChildren: true, + }); + if (exposesNodes.length > 0) { + isRemote = true; + } + + let result: IsHostRemoteConfigResult = + isHost && isRemote ? 'both' : isHost ? 'host' : isRemote ? 'remote' : false; + return result; +} + +export function getRemotesFromHost(ast: SourceFile) { + const remotesObjectNodes = tsquery(ast, REMOTES_EXPRESSION_SELECTOR, { + visitAllChildren: true, + }); + if (remotesObjectNodes.length === 0) { + return []; + } + + const remotesNodes = tsquery(remotesObjectNodes[0], PROPERTY_SELECTOR, { + visitAllChildren: true, + }); + + if (remotesNodes.length === 0) { + return []; + } + + const remotes = []; + for (const remoteNode of remotesNodes) { + const remoteText = remoteNode.getText(); + const remoteParts = remoteText + .split(':') + .map((part) => part.trim().replace(/'/g, '')); + const remoteName = remoteParts.shift(); + const remoteLocation = remoteParts.join(':').replace(/\/[^\/]+$/, ''); + remotes.push([remoteName, remoteLocation]); + } + return remotes; +} + +export function getExposedModulesFromRemote(ast: SourceFile) { + const exposesObjectNodes = tsquery(ast, EXPOSES_EXPRESSION_SELECTOR, { + visitAllChildren: true, + }); + if (exposesObjectNodes.length === 0) { + return {}; + } + + return exposesObjectNodes[0].getText(); +} diff --git a/packages/angular/src/generators/move-to-with-mf/lib/parse-ast-webpack-config.ts b/packages/angular/src/generators/move-to-with-mf/lib/parse-ast-webpack-config.ts new file mode 100644 index 00000000000000..f86126c85f63d3 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/parse-ast-webpack-config.ts @@ -0,0 +1,16 @@ +import type { Tree } from '@nrwl/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function parseASTOfWebpackConfig( + tree: Tree, + pathToWebpackConfig: string +) { + if (!tree.exists(pathToWebpackConfig)) { + throw new Error( + `Cannot migrate webpack config at \`${pathToWebpackConfig}\` as it does not exist. Please ensure this file exists and that the path to the file is correct.` + ); + } + + const source = tree.read(pathToWebpackConfig, 'utf-8'); + return tsquery.ast(source); +} diff --git a/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.spec.ts b/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.spec.ts new file mode 100644 index 00000000000000..a89db33da573dc --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.spec.ts @@ -0,0 +1,87 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; +import { writeNewWebpackConfig } from './write-new-webpack-config'; + +describe('writeNewWebpackConfig', () => { + it('should convert host config correctly', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = writeNewWebpackConfig(ast, 'host', 'host1'); + + // ASSERT + expect(result).toMatchSnapshot(); + }); + + it('should convert remote config correctly', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = writeNewWebpackConfig(ast, 'remote', 'remote1'); + + // ASSERT + expect(result).toMatchSnapshot(); + }); + + it('should convert config that is both remote and host correctly', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs' + }, + exposes: { + './Module': 'apps/both/src/app/remote-entry/entry.module.ts' + } + }) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = writeNewWebpackConfig(ast, 'both', 'both1'); + + // ASSERT + expect(result).toMatchSnapshot(); + }); + + it('should convert config that is neither remote and host correctly', () => { + // ARRANGE + const sourceText = `module.exports = { + plugins: [ + new ModuleFederationPlugin({}) + ] + }`; + + const ast = tsquery.ast(sourceText); + + // ACT + const result = writeNewWebpackConfig(ast, false, 'neither'); + + // ASSERT + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.ts b/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.ts new file mode 100644 index 00000000000000..513069c0a6eb43 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/lib/write-new-webpack-config.ts @@ -0,0 +1,61 @@ +import type { SourceFile } from 'typescript'; +import { + getExposedModulesFromRemote, + getRemotesFromHost, + IsHostRemoteConfigResult, +} from './is-host-remote-config'; + +export function writeNewWebpackConfig( + ast: SourceFile, + mfType: IsHostRemoteConfigResult, + projectName: string +) { + let webpackConfig = ''; + if (!mfType) { + webpackConfig = `const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: '${projectName}', + });`; + } + + if (mfType === 'host') { + const remotes = hostRemotesToString(ast); + webpackConfig = `const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: '${projectName}', + remotes: ${remotes}, + });`; + } + + if (mfType === 'remote') { + const exposedModules = getExposedModulesFromRemote(ast); + webpackConfig = `const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: '${projectName}', + exposes: ${exposedModules}, + });`; + } + + if (mfType === 'both') { + const remotes = hostRemotesToString(ast); + const exposedModules = getExposedModulesFromRemote(ast); + webpackConfig = `const { withModuleFederation } = require('@nrwl/angular/module-federation'); + module.exports = withModuleFederation({ + name: '${projectName}', + remotes: ${remotes}, + exposes: ${exposedModules}, + });`; + } + + return webpackConfig; +} + +function hostRemotesToString(ast: SourceFile) { + const remotes: string = getRemotesFromHost(ast) + .reduce( + (acc, remotePair) => `['${remotePair[0]}', '${remotePair[1]}'], ${acc}`, + '' + ) + .trim(); + return `[${remotes.endsWith(',') ? remotes.slice(0, -1) : remotes}]`; +} diff --git a/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.compat.ts b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.compat.ts new file mode 100644 index 00000000000000..0201dbe10e7296 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import moveToWithMF from './move-to-with-mf'; + +export default convertNxGenerator(moveToWithMF); diff --git a/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.spec.ts b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.spec.ts new file mode 100644 index 00000000000000..dde70655668bfa --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.spec.ts @@ -0,0 +1,166 @@ +import { addProjectConfiguration } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + STANDARD_HOST_MFE_CONFIG, + STANDARD_REMOTE_MFE_CONFIG, + OLD_OBJECT_SHARED_SYNTAX, + ERROR_NAME_DOESNT_MATCH, + ERROR_SHARED_PACKAGES_DOESNT_MATCH, +} from './move-to-with-mf.test-data'; +import moveToWithMF from './move-to-with-mf'; + +describe('moveToWithMF', () => { + it('should migrate a standard previous generated host config correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'host1', { + name: 'host1', + root: 'apps/host1', + sourceRoot: 'apps/host1/src', + targets: { + build: { + executor: '@nrwl/angular:webpack-browser', + options: { + customWebpackConfig: { + path: 'apps/host1/webpack.config.js', + }, + }, + }, + }, + }); + + tree.write('apps/host1/webpack.config.js', STANDARD_HOST_MFE_CONFIG); + + // ACT + await moveToWithMF(tree, { + project: 'host1', + }); + + // ASSERT + const webpackSource = tree.read('apps/host1/webpack.config.js', 'utf-8'); + expect(webpackSource).toMatchSnapshot(); + }); + + it('should migrate a standard previous generated remote config correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'remote1', { + name: 'remote1', + root: 'apps/remote1', + sourceRoot: 'apps/remote1/src', + targets: { + build: { + executor: '@nrwl/angular:webpack-browser', + options: { + customWebpackConfig: { + path: 'apps/remote1/webpack.config.js', + }, + }, + }, + }, + }); + + tree.write('apps/remote1/webpack.config.js', STANDARD_REMOTE_MFE_CONFIG); + + // ACT + await moveToWithMF(tree, { + project: 'remote1', + }); + + // ASSERT + const webpackSource = tree.read('apps/remote1/webpack.config.js', 'utf-8'); + expect(webpackSource).toMatchSnapshot(); + }); + + it('should migrate a standard previous generated remote config using object shared syntax correctly', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'remote1', { + name: 'remote1', + root: 'apps/remote1', + sourceRoot: 'apps/remote1/src', + targets: { + build: { + executor: '@nrwl/angular:webpack-browser', + options: { + customWebpackConfig: { + path: 'apps/remote1/webpack.config.js', + }, + }, + }, + }, + }); + + tree.write('apps/remote1/webpack.config.js', OLD_OBJECT_SHARED_SYNTAX); + + // ACT + await moveToWithMF(tree, { + project: 'remote1', + }); + + // ASSERT + const webpackSource = tree.read('apps/remote1/webpack.config.js', 'utf-8'); + expect(webpackSource).toMatchSnapshot(); + }); + + it('should throw when the uniqueName doesnt match the project name', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'remote1', { + name: 'remote1', + root: 'apps/remote1', + sourceRoot: 'apps/remote1/src', + targets: { + build: { + executor: '@nrwl/angular:webpack-browser', + options: { + customWebpackConfig: { + path: 'apps/remote1/webpack.config.js', + }, + }, + }, + }, + }); + + tree.write('apps/remote1/webpack.config.js', ERROR_NAME_DOESNT_MATCH); + + // ACT & ASSERT + await expect( + moveToWithMF(tree, { + project: 'remote1', + }) + ).rejects.toThrow(); + }); + + it('should throw when the shared npm packages configs has been modified', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'host1', { + name: 'host1', + root: 'apps/host1', + sourceRoot: 'apps/host1/src', + targets: { + build: { + executor: '@nrwl/angular:webpack-browser', + options: { + customWebpackConfig: { + path: 'apps/host1/webpack.config.js', + }, + }, + }, + }, + }); + + tree.write( + 'apps/host1/webpack.config.js', + ERROR_SHARED_PACKAGES_DOESNT_MATCH + ); + + // ACT & ASSERT + await expect( + moveToWithMF(tree, { + project: 'host1', + }) + ).rejects.toThrow(); + }); +}); diff --git a/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.test-data.ts b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.test-data.ts new file mode 100644 index 00000000000000..04ca8907cd4961 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.test-data.ts @@ -0,0 +1,449 @@ +export const STANDARD_HOST_MFE_CONFIG = `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: 'host1', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs', + }, + 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(), + ], +}; +`; + +export const STANDARD_REMOTE_MFE_CONFIG = `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: 'remote1', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'remote1', + filename: 'remoteEntry.js', + exposes: { + './Module': 'apps/remote1/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(), + ], +}; +`; + +export const OLD_OBJECT_SHARED_SYNTAX = `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: 'remote1', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'remote1', + filename: 'remoteEntry.js', + exposes: { + './Module': 'apps/remote1/src/app/remote-entry/entry.module.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, + }, + rxjs: { + singleton: true, + strictVersion: true, + }, + ...sharedMappings.getDescriptors(), + }, + library: { + type: 'module', + }, + }), + sharedMappings.getPlugin(), + ], +};`; + +export const ERROR_NAME_DOESNT_MATCH = `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: 'somethingelse', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'remote1', + filename: 'remoteEntry.js', + exposes: { + './Module': 'apps/remote1/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(), + ], +}; +`; + +export const ERROR_SHARED_PACKAGES_DOESNT_MATCH = `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: 'host1', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + }, + experiments: { + outputModule: true, + }, + resolve: { + alias: { + ...sharedMappings.getAliases(), + }, + }, + plugins: [ + new ModuleFederationPlugin({ + remotes: { + remote1: 'http://localhost:4201/remoteEntry.mjs', + }, + shared: share({ + '@angular/core': { + singleton: true, + strictVersion: true, + requiredVersion: '13.0.0', + includeSecondaries: true, + }, + '@angular/common': { + singleton: true, + strictVersion: true, + requiredVersion: 'auto', + includeSecondaries: true, + }, + '@angular/common/http': { + singleton: true, + strictVersion: false, + 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/move-to-with-mf/move-to-with-mf.ts b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.ts new file mode 100644 index 00000000000000..c527e5c250a60e --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/move-to-with-mf.ts @@ -0,0 +1,50 @@ +import type { Tree } from '@nrwl/devkit'; +import type { Schema } from './schema'; + +import { readProjectConfiguration, formatFiles } from '@nrwl/devkit'; +import { getMfeProjects } from '../../utils/get-mfe-projects'; +import { + checkOutputNameMatchesProjectName, + checkSharedNpmPackagesMatchExpected, + getWebpackConfigPath, + isHostRemoteConfig, + parseASTOfWebpackConfig, + writeNewWebpackConfig, +} from './lib'; + +export default async function moveToWithMF(tree: Tree, schema: Schema) { + const projects = new Set(getMfeProjects(tree)); + + if (!projects.has(schema.project)) { + throw new Error( + `Could not find project \`${schema.project}\` in your workspace. Please check the name of the project you're wishing to convert exists.` + ); + } + + const project = readProjectConfiguration(tree, schema.project); + const pathToWebpackConfig = getWebpackConfigPath(project, schema.project); + const webpackAst = parseASTOfWebpackConfig(tree, pathToWebpackConfig); + + if (!checkOutputNameMatchesProjectName(webpackAst, schema.project)) { + throw new Error( + `Cannot automatically migrate \`${schema.project}\` to \`withModuleFederation\` mirco frontend webpack config. + \`uniqueName\` in webpack config (${pathToWebpackConfig}) does not match project name.` + ); + } + + if (!checkSharedNpmPackagesMatchExpected(webpackAst)) { + throw new Error( + `Cannot automatically migrate \`${schema.project}\` to \`withModuleFederation\` mirco frontend webpack config. + There are npm packages being shared with a custom configuration in webpack config (${pathToWebpackConfig}).` + ); + } + + const updatedWebpackConfig = writeNewWebpackConfig( + webpackAst, + isHostRemoteConfig(webpackAst), + schema.project + ); + tree.write(pathToWebpackConfig, updatedWebpackConfig); + + await formatFiles(tree); +} diff --git a/packages/angular/src/generators/move-to-with-mf/schema.d.ts b/packages/angular/src/generators/move-to-with-mf/schema.d.ts new file mode 100644 index 00000000000000..e76548202a2ae4 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/schema.d.ts @@ -0,0 +1,3 @@ +export interface Schema { + project: string; +} diff --git a/packages/angular/src/generators/move-to-with-mf/schema.json b/packages/angular/src/generators/move-to-with-mf/schema.json new file mode 100644 index 00000000000000..95ea4bc093ad33 --- /dev/null +++ b/packages/angular/src/generators/move-to-with-mf/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "MoveToWithMFGenerator", + "cli": "nx", + "title": "Move to withModuleFederation Generator Options Schema", + "type": "object", + "description": "Migrates an existing micro frontend config to withModuleFederation config", + "additionalProperties": false, + "properties": { + "project": { + "type": "string", + "description": "The name of the micro frontend project to migrate.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What micro frontend project would you ike to migrate?" + } + } +}