From 608926a8b0a3bfeba896fdbcbdb0d5914460ff10 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 30 Oct 2024 17:02:18 +0000 Subject: [PATCH 1/6] feat(angular): use module-federation runtime for dynamic federation --- packages/angular/mf/mf.ts | 12 +++++ .../__snapshots__/setup-mf.spec.ts.snap | 30 +++++------ .../setup-mf/lib/add-remote-to-host.ts | 50 ++++++++++++++----- .../generators/setup-mf/lib/fix-bootstrap.ts | 5 +- .../src/generators/setup-mf/setup-mf.spec.ts | 8 +-- 5 files changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/angular/mf/mf.ts b/packages/angular/mf/mf.ts index 8184174b9e909..a85cdae684807 100644 --- a/packages/angular/mf/mf.ts +++ b/packages/angular/mf/mf.ts @@ -7,6 +7,9 @@ declare const __webpack_share_scopes__: { default: unknown }; let resolveRemoteUrl: ResolveRemoteUrlFunction; +/** + * @deprecated Use Runtime Helpers from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + */ export function setRemoteUrlResolver( _resolveRemoteUrl: ResolveRemoteUrlFunction ) { @@ -15,10 +18,16 @@ export function setRemoteUrlResolver( let remoteUrlDefinitions: Record; +/** + * @deprecated Use init() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + */ export function setRemoteDefinitions(definitions: Record) { remoteUrlDefinitions = definitions; } +/** + * @deprecated Use registerRemotes() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + */ export function setRemoteDefinition(remoteName: string, remoteUrl: string) { remoteUrlDefinitions ??= {}; remoteUrlDefinitions[remoteName] = remoteUrl; @@ -27,6 +36,9 @@ export function setRemoteDefinition(remoteName: string, remoteUrl: string) { let remoteModuleMap = new Map(); let remoteContainerMap = new Map(); +/** + * @deprecated Use loadRemote() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + */ export async function loadRemoteModule(remoteName: string, moduleName: string) { const remoteModuleKey = `${remoteName}:${moduleName}`; if (remoteModuleMap.has(remoteModuleKey)) { diff --git a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap index 8e908a8bc759d..ff2d643486ec5 100644 --- a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap +++ b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap @@ -1,32 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Init MF --federationType=dynamic should create a host with the correct configurations 1`] = ` -"import { setRemoteDefinitions } from '@nx/angular/mf'; +"import { init } from '@module-federation/enhanced/runtime'; fetch('/module-federation.manifest.json') .then((res) => res.json()) - .then(definitions => setRemoteDefinitions(definitions)) + .then((remotes: Record) => Object.entries(remotes).map(([name, entry]) => ({ name,entry}))) + .then(remotes => init({name: 'app1', remotes})) .then(() => import('./bootstrap').catch(err => console.error(err)));" `; exports[`Init MF --federationType=dynamic should create a host with the correct configurations when --typescriptConfiguration=true 1`] = ` -"import { setRemoteDefinitions } from '@nx/angular/mf'; +"import { init } from '@module-federation/enhanced/runtime'; fetch('/module-federation.manifest.json') .then((res) => res.json()) - .then(definitions => setRemoteDefinitions(definitions)) + .then((remotes: Record) => Object.entries(remotes).map(([name, entry]) => ({ name,entry}))) + .then(remotes => init({name: 'app1', remotes})) .then(() => import('./bootstrap').catch(err => console.error(err)));" `; exports[`Init MF --federationType=dynamic should wire up existing remote to dynamic host correctly 1`] = ` "import { NxWelcomeComponent } from './nx-welcome.component'; import { Route } from '@angular/router'; -import { loadRemoteModule } from '@nx/angular/mf'; +import { loadRemote } from '@module-federation/enhanced/runtime'; export const appRoutes: Route[] = [ { path: 'remote1', - loadChildren: () => loadRemoteModule('remote1', './Module').then(m => m.RemoteEntryModule) + loadChildren: () => loadRemote('remote1/Module').then(m => m!.RemoteEntryModule) }, { path: '', @@ -38,12 +40,12 @@ export const appRoutes: Route[] = [ exports[`Init MF --federationType=dynamic should wire up existing remote to dynamic host correctly when --typescriptConfiguration=true 1`] = ` "import { NxWelcomeComponent } from './nx-welcome.component'; import { Route } from '@angular/router'; -import { loadRemoteModule } from '@nx/angular/mf'; +import { loadRemote } from '@module-federation/enhanced/runtime'; export const appRoutes: Route[] = [ { path: 'remote1', - loadChildren: () => loadRemoteModule('remote1', './Module').then(m => m.RemoteEntryModule) + loadChildren: () => loadRemote('remote1/Module').then(m => m!.RemoteEntryModule) }, { path: '', @@ -59,11 +61,11 @@ import { Route } from '@angular/router'; export const appRoutes: Route[] = [ { path: 'remote2', - loadChildren: () => import('remote2/Module').then(m => m.RemoteEntryModule) + loadChildren: () => import('remote2/Module').then(m => m!.RemoteEntryModule) }, { path: 'remote1', - loadChildren: () => import('remote1/Module').then(m => m.RemoteEntryModule) + loadChildren: () => import('remote1/Module').then(m => m!.RemoteEntryModule) }, { path: '', @@ -175,12 +177,12 @@ export default config; exports[`Init MF should add a remote to dynamic host correctly 1`] = ` "import { NxWelcomeComponent } from './nx-welcome.component'; import { Route } from '@angular/router'; -import { loadRemoteModule } from '@nx/angular/mf'; +import { loadRemote } from '@module-federation/enhanced/runtime'; export const appRoutes: Route[] = [ { path: 'remote1', - loadChildren: () => loadRemoteModule('remote1', './Module').then(m => m.RemoteEntryModule) + loadChildren: () => loadRemote('remote1/Module').then(m => m!.RemoteEntryModule) }, { path: '', @@ -192,12 +194,12 @@ export const appRoutes: Route[] = [ exports[`Init MF should add a remote to dynamic host correctly when --typescriptConfiguration=true 1`] = ` "import { NxWelcomeComponent } from './nx-welcome.component'; import { Route } from '@angular/router'; -import { loadRemoteModule } from '@nx/angular/mf'; +import { loadRemote } from '@module-federation/enhanced/runtime'; export const appRoutes: Route[] = [ { path: 'remote1', - loadChildren: () => loadRemoteModule('remote1', './Module').then(m => m.RemoteEntryModule) + loadChildren: () => loadRemote('remote1/Module').then(m => m!.RemoteEntryModule) }, { path: '', diff --git a/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts b/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts index 9e8c57d752550..c8cb6e2fa7f57 100644 --- a/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts +++ b/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts @@ -47,7 +47,12 @@ export function addRemoteToHost(tree: Tree, options: AddRemoteOptions) { isHostUsingTypescriptConfig ); } else if (hostFederationType === 'dynamic') { - addRemoteToDynamicHost(tree, options, pathToMFManifest); + addRemoteToDynamicHost( + tree, + options, + pathToMFManifest, + hostProject.sourceRoot + ); } addLazyLoadedRouteToHostAppModule(tree, options, hostFederationType); @@ -114,17 +119,23 @@ function addRemoteToStaticHost( function addRemoteToDynamicHost( tree: Tree, options: AddRemoteOptions, - pathToMfManifest: string + pathToMfManifest: string, + hostSourceRoot: string ) { + // TODO(Colum): Remove for Nx 22 + const usingLegacyDynamicFederation = tree + .read(`${hostSourceRoot}/main.ts`, 'utf-8') + .includes('setRemoteDefinitions('); updateJson(tree, pathToMfManifest, (manifest) => { return { ...manifest, - [options.appName]: `http://localhost:${options.port}`, + [options.appName]: `http://localhost:${options.port}${ + usingLegacyDynamicFederation ? '' : '/mf-manifest.json' + }`, }; }); } -// TODO(colum): future work: allow dev to pass to path to routing module function addLazyLoadedRouteToHostAppModule( tree: Tree, options: AddRemoteOptions, @@ -150,13 +161,22 @@ function addLazyLoadedRouteToHostAppModule( true ); + // TODO(Colum): Remove for Nx 22 + const usingLegacyDynamicFederation = + hostFederationType === 'dynamic' && + tree + .read(`${hostAppConfig.sourceRoot}/main.ts`, 'utf-8') + .includes('setRemoteDefinitions('); + if (hostFederationType === 'dynamic') { sourceFile = insertImport( tree, sourceFile, pathToHostRootRouting, - 'loadRemoteModule', - '@nx/angular/mf' + usingLegacyDynamicFederation ? 'loadRemoteModule' : 'loadRemote', + usingLegacyDynamicFederation + ? '@nx/angular/mf' + : '@module-federation/enhanced/runtime' ); } @@ -164,20 +184,26 @@ function addLazyLoadedRouteToHostAppModule( const exportedRemote = options.standalone ? 'remoteRoutes' : 'RemoteEntryModule'; + const remoteModulePath = `${options.appName.replace( + /-/g, + '_' + )}/${routePathName}`; const routeToAdd = hostFederationType === 'dynamic' - ? `loadRemoteModule('${options.appName.replace( - /-/g, - '_' - )}', './${routePathName}')` - : `import('${options.appName.replace(/-/g, '_')}/${routePathName}')`; + ? usingLegacyDynamicFederation + ? `loadRemoteModule('${options.appName.replace( + /-/g, + '_' + )}', './${routePathName}')` + : `loadRemote('${remoteModulePath}')` + : `import('${remoteModulePath}')`; addRoute( tree, pathToHostRootRouting, `{ path: '${options.appName}', - loadChildren: () => ${routeToAdd}.then(m => m.${exportedRemote}) + loadChildren: () => ${routeToAdd}.then(m => m!.${exportedRemote}) }` ); diff --git a/packages/angular/src/generators/setup-mf/lib/fix-bootstrap.ts b/packages/angular/src/generators/setup-mf/lib/fix-bootstrap.ts index 9dca07307885a..271e20fd9dec8 100644 --- a/packages/angular/src/generators/setup-mf/lib/fix-bootstrap.ts +++ b/packages/angular/src/generators/setup-mf/lib/fix-bootstrap.ts @@ -23,11 +23,12 @@ export function fixBootstrap(tree: Tree, appRoot: string, options: Schema) { manifestPath = '/module-federation.manifest.json'; } - const fetchMFManifestCode = `import { setRemoteDefinitions } from '@nx/angular/mf'; + const fetchMFManifestCode = `import { init } from '@module-federation/enhanced/runtime'; fetch('${manifestPath}') .then((res) => res.json()) - .then(definitions => setRemoteDefinitions(definitions)) + .then((remotes: Record) => Object.entries(remotes).map(([name, entry]) => ({ name,entry}))) + .then(remotes => init({name: '${options.appName}', remotes})) .then(() => ${bootstrapImportCode});`; tree.write(mainFilePath, fetchMFManifestCode); diff --git a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts index 5490e9e781c11..c99bb98a6e81b 100644 --- a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts +++ b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts @@ -574,7 +574,7 @@ describe('Init MF', () => { expect( readJson(tree, 'app1/public/module-federation.manifest.json') ).toEqual({ - remote1: 'http://localhost:4201', + remote1: 'http://localhost:4201/mf-manifest.json', }); expect( tree.read('app1/src/app/app.routes.ts', 'utf-8') @@ -609,7 +609,7 @@ describe('Init MF', () => { expect( readJson(tree, 'app1/public/module-federation.manifest.json') ).toEqual({ - remote1: 'http://localhost:4201', + remote1: 'http://localhost:4201/mf-manifest.json', }); expect( tree.read('app1/src/app/app.routes.ts', 'utf-8') @@ -648,7 +648,7 @@ describe('Init MF', () => { expect( readJson(tree, 'app1/public/module-federation.manifest.json') ).toEqual({ - remote1: 'http://localhost:4201', + remote1: 'http://localhost:4201/mf-manifest.json', }); expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot(); }); @@ -684,7 +684,7 @@ describe('Init MF', () => { expect( readJson(tree, 'app1/public/module-federation.manifest.json') ).toEqual({ - remote1: 'http://localhost:4201', + remote1: 'http://localhost:4201/mf-manifest.json', }); expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot(); }); From f924a24eca503438e9d5c7a21d6a02289f77871c Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 10:37:20 +0000 Subject: [PATCH 2/6] docs(angular): update deprecation message --- packages/angular/mf/mf.ts | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/angular/mf/mf.ts b/packages/angular/mf/mf.ts index a85cdae684807..eab3f1d8da76e 100644 --- a/packages/angular/mf/mf.ts +++ b/packages/angular/mf/mf.ts @@ -20,6 +20,26 @@ let remoteUrlDefinitions: Record; /** * @deprecated Use init() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you have a remote app called `my-remote-app` and you want to use the `http://localhost:4201/mf-manifest.json` as the remote url, you should change it from: + * ```ts + * import { setRemoteDefinitions } from '@nx/angular/mf'; + * + * setRemoteDefinitions({ + * 'my-remote-app': 'http://localhost:4201/mf-manifest.json' + * }); + * ``` + * to use init(): + * ```ts + * import { init } from '@module-federation/enhanced/runtime'; + * + * init({ + * name: 'host', + * remotes: [{ + * name: 'my-remote-app', + * entry: 'http://localhost:4201/mf-manifest.json' + * }] + * }); + * ``` */ export function setRemoteDefinitions(definitions: Record) { remoteUrlDefinitions = definitions; @@ -27,6 +47,26 @@ export function setRemoteDefinitions(definitions: Record) { /** * @deprecated Use registerRemotes() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you set a remote app with `setRemoteDefinition` such as: + * ```ts + * import { setRemoteDefinition } from '@nx/angular/mf'; + * + * setRemoteDefinition( + * 'my-remote-app', + * 'http://localhost:4201/mf-manifest.json' + * ); + * ``` + * change it to use registerRemotes(): + * ```ts + * import { registerRemotes } from '@module-federation/enhanced/runtime'; + * + * registerRemotes([ + * { + * name: 'my-remote-app', + * entry: 'http://localhost:4201/mf-manifest.json' + * } + * ]); + * ``` */ export function setRemoteDefinition(remoteName: string, remoteUrl: string) { remoteUrlDefinitions ??= {}; @@ -38,6 +78,18 @@ let remoteContainerMap = new Map(); /** * @deprecated Use loadRemote() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you set a load a remote with `loadRemoteModule` such as: + * ```ts + * import { loadRemoteModule } from '@nx/angular/mf'; + * + * loadRemoteModule('my-remote-app', './Module').then(m => m.RemoteEntryModule); + * ``` + * change it to use loadRemote(): + * ```ts + * import { loadRemote } from '@module-federation/enhanced/runtime'; + * + * loadRemote('my-remote-app/Module').then(m => m.RemoteEntryModule); + * ``` */ export async function loadRemoteModule(remoteName: string, moduleName: string) { const remoteModuleKey = `${remoteName}:${moduleName}`; From 8b277eb1c31fad756e1d4f5865769954279d81ac Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 11:10:08 +0000 Subject: [PATCH 3/6] feat(react): use mf runtime for dynamic federation --- .../src/generators/setup-mf/setup-mf.ts | 5 +- .../__snapshots__/host.rspack.spec.ts.snap | 178 ++++++++++++++++++ .../src/app/__fileName__.tsx__tmpl__ | 16 +- .../host/files/common-ts/src/main.ts__tmpl__ | 17 +- .../common/src/app/__fileName__.js__tmpl__ | 15 +- .../host/files/common/src/main.js__tmpl__ | 17 +- .../src/app/__fileName__.jsx__tmpl__ | 17 +- .../files/rspack-common/src/main.jsx__tmpl__ | 17 +- .../src/generators/host/host.rspack.spec.ts | 77 +++++++- packages/react/src/generators/host/host.ts | 11 +- .../host/lib/add-module-federation-files.ts | 5 +- 11 files changed, 327 insertions(+), 48 deletions(-) create mode 100644 packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap diff --git a/packages/angular/src/generators/setup-mf/setup-mf.ts b/packages/angular/src/generators/setup-mf/setup-mf.ts index 5637e1cd9549d..bed23cc855fd9 100644 --- a/packages/angular/src/generators/setup-mf/setup-mf.ts +++ b/packages/angular/src/generators/setup-mf/setup-mf.ts @@ -45,11 +45,12 @@ export async function setupMf(tree: Tree, rawOptions: Schema) { if (!options.skipPackageJson) { installTask = addDependenciesToPackageJson( tree, - {}, + { + '@module-federation/enhanced': moduleFederationEnhancedVersion, + }, { '@nx/web': nxVersion, '@nx/webpack': nxVersion, - '@module-federation/enhanced': moduleFederationEnhancedVersion, } ); } diff --git a/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap b/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap new file mode 100644 index 0000000000000..c38adcb67d1f1 --- /dev/null +++ b/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hostGenerator bundler=rspack should generate host files and configs for SSR 1`] = ` +"const { composePlugins, withNx, withReact } = require('@nx/rspack'); +const { withModuleFederationForSSR } = require('@nx/rspack/module-federation'); + +const baseConfig = require('./module-federation.config'); + +const defaultConfig = { + ...baseConfig, +}; + +// Nx plugins for rspack to build config object from Nx options and context. +/** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ +module.exports = composePlugins( + withNx(), + withReact({ ssr: true }), + withModuleFederationForSSR(defaultConfig, { dts: false }) +); +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs for SSR 2`] = ` +"// @ts-check + +/** + * @type {import('@nx/rspack/module-federation').ModuleFederationConfig} + **/ +const moduleFederationConfig = { + name: 'test', + remotes: [], +}; + +/** + * Nx requires a default export of the config to allow correct resolution of the module federation graph. + **/ +module.exports = moduleFederationConfig; +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs for SSR when --typescriptConfiguration=true 1`] = ` +"import { composePlugins, withNx, withReact } from '@nx/rspack'; +import { withModuleFederationForSSR } from '@nx/rspack/module-federation'; + +import baseConfig from './module-federation.config'; + +const defaultConfig = { + ...baseConfig, +}; + +// Nx plugins for rspack to build config object from Nx options and context. +/** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ +export default composePlugins( + withNx(), + withReact({ ssr: true }), + withModuleFederationForSSR(defaultConfig, { dts: false }) +); +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = ` +"import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + +const config: ModuleFederationConfig = { + name: 'test', + remotes: [], +}; + +/** + * Nx requires a default export of the config to allow correct resolution of the module federation graph. + **/ +export default config; +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=false 1`] = ` +"const { composePlugins, withNx, withReact } = require('@nx/rspack'); +const { withModuleFederation } = require('@nx/rspack/module-federation'); + +const baseConfig = require('./module-federation.config'); + +const config = { + ...baseConfig, +}; + +// Nx plugins for rspack to build config object from Nx options and context. +/** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ +module.exports = composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }) +); +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=false 2`] = ` +"/** + * Nx requires a default export of the config to allow correct resolution of the module federation graph. + **/ +module.exports = { + name: 'test', + /** + * To use a remote that does not exist in your current Nx Workspace + * You can use the tuple-syntax to define your remote + * + * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']] + * + * You _may_ need to add a \`remotes.d.ts\` file to your \`src/\` folder declaring the external remote for tsc, with the + * following content: + * + * declare module 'my-external-remote'; + * + */ + remotes: [], +}; +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=true 1`] = ` +"import {composePlugins, withNx, withReact} from '@nx/rspack'; +import {withModuleFederation, ModuleFederationConfig} from '@nx/rspack/module-federation'; + +import baseConfig from './module-federation.config'; + +const config: ModuleFederationConfig = { + ...baseConfig, +}; + +// Nx plugins for rspack to build config object from Nx options and context. +/** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ +export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false })); +" +`; + +exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=true 2`] = ` +"import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + +const config: ModuleFederationConfig = { + name: 'test', + /** + * To use a remote that does not exist in your current Nx Workspace + * You can use the tuple-syntax to define your remote + * + * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']] + * + * You _may_ need to add a \`remotes.d.ts\` file to your \`src/\` folder declaring the external remote for tsc, with the + * following content: + * + * declare module 'my-external-remote'; + * + */ + remotes: [ + + ], +}; + +/** +* Nx requires a default export of the config to allow correct resolution of the module federation graph. +**/ +export default config; +" +`; diff --git a/packages/react/src/generators/host/files/common-ts/src/app/__fileName__.tsx__tmpl__ b/packages/react/src/generators/host/files/common-ts/src/app/__fileName__.tsx__tmpl__ index 2c6a48fdfca44..f4789e0395ff6 100644 --- a/packages/react/src/generators/host/files/common-ts/src/app/__fileName__.tsx__tmpl__ +++ b/packages/react/src/generators/host/files/common-ts/src/app/__fileName__.tsx__tmpl__ @@ -4,17 +4,17 @@ import NxWelcome from "./nx-welcome"; <%_ } _%> import { Link, Route, Routes } from 'react-router-dom'; <%_ if (dynamic) { _%> -import { loadRemoteModule } from '@nx/react/mf'; +import { loadRemote } from '@module-federation/enhanced/runtime'; <%_ } _%> <%_ if (remotes.length > 0) { - remotes.forEach(function(r) { - if (dynamic) { _%> -const <%= r.className %> = React.lazy(() => loadRemoteModule('<%= r.fileName %>', './Module')) - <%_ } else { _%> -const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); - <%_ } _%> - <%_ }); _%> + remotes.forEach(function(r) { _%> +<%_ if (dynamic) { _%> + const <%= r.className %> = React.lazy(() => loadRemote('<%= r.fileName %>/Module') as any) +<%_ } else { _%> + const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); +<%_ } _%> + <%_ }); _%> <%_ } _%> export function App() { diff --git a/packages/react/src/generators/host/files/common-ts/src/main.ts__tmpl__ b/packages/react/src/generators/host/files/common-ts/src/main.ts__tmpl__ index 28b8cfb24dcb8..52c51d1e1dfd4 100644 --- a/packages/react/src/generators/host/files/common-ts/src/main.ts__tmpl__ +++ b/packages/react/src/generators/host/files/common-ts/src/main.ts__tmpl__ @@ -1,10 +1,13 @@ <%_ if (dynamic) { _%> -import { setRemoteDefinitions } from '@nx/react/mf'; + import { init } from '@module-federation/enhanced/runtime'; -fetch('/assets/module-federation.manifest.json') -.then((res) => res.json()) -.then(definitions => setRemoteDefinitions(definitions)) -.then(() => import('./bootstrap').catch(err => console.error(err))); + fetch('/assets/module-federation.manifest.json') + .then((res) => res.json()) + .then((remotes: Record) => + Object.entries(remotes).map(([name, entry]) => ({ name, entry })) + ) + .then((remotes) => init({ name: '<%= projectName %>', remotes })) + .then(() => import('./bootstrap').catch(err => console.error(err))); <%_ } else { _%> -import('./bootstrap').catch(err => console.error(err)); -<%_ } _%> \ No newline at end of file + import('./bootstrap').catch(err => console.error(err)); +<%_ } _%> diff --git a/packages/react/src/generators/host/files/common/src/app/__fileName__.js__tmpl__ b/packages/react/src/generators/host/files/common/src/app/__fileName__.js__tmpl__ index 8252e92801c59..ab542c9315a48 100644 --- a/packages/react/src/generators/host/files/common/src/app/__fileName__.js__tmpl__ +++ b/packages/react/src/generators/host/files/common/src/app/__fileName__.js__tmpl__ @@ -3,12 +3,19 @@ import * as React from 'react'; import NxWelcome from "./nx-welcome"; <%_ } _%> import { Link, Route, Routes } from 'react-router-dom'; +<%_ if (dynamic) { _%> +import { loadRemote } from '@module-federation/enhanced/runtime'; +<%_ } _%> <%_ if (remotes.length > 0) { - remotes.forEach(function(r) { _%> -const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); -<%_ }); _%> -<% } %> + remotes.forEach(function(r) { _%> +<%_ if (dynamic) { _%> + const <%= r.className %> = React.lazy(() => loadRemote('<%= r.fileName %>/Module') as any) +<%_ } else { _%> + const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); +<%_ } _%> + <%_ }); _%> +<%_ } _%> export function App() { return ( diff --git a/packages/react/src/generators/host/files/common/src/main.js__tmpl__ b/packages/react/src/generators/host/files/common/src/main.js__tmpl__ index f68313e1f932e..52c51d1e1dfd4 100644 --- a/packages/react/src/generators/host/files/common/src/main.js__tmpl__ +++ b/packages/react/src/generators/host/files/common/src/main.js__tmpl__ @@ -1,10 +1,13 @@ <%_ if (dynamic) { _%> -import { setRemoteDefinitions } from '@nx/react/mf'; + import { init } from '@module-federation/enhanced/runtime'; -fetch('/assets/module-federation.manifest.json') -.then((res) => res.json()) -.then(definitions => setRemoteDefinitions(definitions)) -.then(() => import('./bootstrap').catch(err => console.error(err))); + fetch('/assets/module-federation.manifest.json') + .then((res) => res.json()) + .then((remotes: Record) => + Object.entries(remotes).map(([name, entry]) => ({ name, entry })) + ) + .then((remotes) => init({ name: '<%= projectName %>', remotes })) + .then(() => import('./bootstrap').catch(err => console.error(err))); <%_ } else { _%> -import('./bootstrap').catch(err => console.error(err)); -<%_ } _%> \ No newline at end of file + import('./bootstrap').catch(err => console.error(err)); +<%_ } _%> diff --git a/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__ b/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__ index e0da5bf8c8945..6d2c466714635 100644 --- a/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__ +++ b/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__ @@ -4,11 +4,20 @@ import NxWelcome from "./nx-welcome"; <%_ } _%> import { Link, Route, Routes } from 'react-router-dom'; -<% if (remotes.length > 0) { - remotes.forEach(function(r) { %> -const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); - <%_ }); _%> +<%_ if (dynamic) { _%> +import { loadRemote } from '@module-federation/enhanced/runtime'; <%_ } _%> + +<%_ if (remotes.length > 0) { + remotes.forEach(function(r) { _%> +<%_ if (dynamic) { _%> + const <%= r.className %> = React.lazy(() => loadRemote('<%= r.fileName %>/Module') as any) +<%_ } else { _%> + const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module')); +<%_ } _%> + <%_ }); _%> +<%_ } _%> + export function App() { return ( diff --git a/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__ b/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__ index f68313e1f932e..52c51d1e1dfd4 100644 --- a/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__ +++ b/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__ @@ -1,10 +1,13 @@ <%_ if (dynamic) { _%> -import { setRemoteDefinitions } from '@nx/react/mf'; + import { init } from '@module-federation/enhanced/runtime'; -fetch('/assets/module-federation.manifest.json') -.then((res) => res.json()) -.then(definitions => setRemoteDefinitions(definitions)) -.then(() => import('./bootstrap').catch(err => console.error(err))); + fetch('/assets/module-federation.manifest.json') + .then((res) => res.json()) + .then((remotes: Record) => + Object.entries(remotes).map(([name, entry]) => ({ name, entry })) + ) + .then((remotes) => init({ name: '<%= projectName %>', remotes })) + .then(() => import('./bootstrap').catch(err => console.error(err))); <%_ } else { _%> -import('./bootstrap').catch(err => console.error(err)); -<%_ } _%> \ No newline at end of file + import('./bootstrap').catch(err => console.error(err)); +<%_ } _%> diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts index 2925acf8b316e..c15771daa2fb3 100644 --- a/packages/react/src/generators/host/host.rspack.spec.ts +++ b/packages/react/src/generators/host/host.rspack.spec.ts @@ -85,8 +85,7 @@ jest.mock('@nx/devkit', () => { }; }); -// TODO(colum): turn these on when rspack is moved into the main repo -xdescribe('hostGenerator', () => { +describe('hostGenerator', () => { let tree: Tree; // TODO(@jaysoo): Turn this back to adding the plugin @@ -121,9 +120,9 @@ xdescribe('hostGenerator', () => { expect(tree.exists('test/tsconfig.json')).toBeTruthy(); - expect(tree.exists('test/src/bootstrap.js')).toBeTruthy(); - expect(tree.exists('test/src/main.js')).toBeTruthy(); - expect(tree.exists('test/src/app/app.js')).toBeTruthy(); + expect(tree.exists('test/src/bootstrap.jsx')).toBeTruthy(); + expect(tree.exists('test/src/main.jsx')).toBeTruthy(); + expect(tree.exists('test/src/app/app.jsx')).toBeTruthy(); }); it('should generate host files and configs when --js=false', async () => { @@ -206,6 +205,7 @@ xdescribe('hostGenerator', () => { }); const packageJson = readJson(tree, 'package.json'); + console.log(packageJson); expect(packageJson.devDependencies['@nx/web']).toBeDefined(); }); @@ -363,5 +363,72 @@ xdescribe('hostGenerator', () => { }) ).rejects.toThrowError(`Invalid remote name provided: ${remote}.`); }); + + it('should generate create files with dynamic host', async () => { + const tree = createTreeWithEmptyWorkspace(); + const remote = 'remote1'; + + await hostGenerator(tree, { + directory: 'myhostapp', + remotes: [remote], + dynamic: true, + e2eTestRunner: 'none', + linter: Linter.None, + style: 'css', + unitTestRunner: 'none', + typescriptConfiguration: false, + bundler: 'rspack', + }); + + expect(tree.read('myhostapp/src/main.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { init } from '@module-federation/enhanced/runtime'; + + fetch('/assets/module-federation.manifest.json') + .then((res) => res.json()) + .then((remotes: Record) => + Object.entries(remotes).map(([name, entry]) => ({ name, entry })) + ) + .then((remotes) => init({ name: 'myhostapp', remotes })) + .then(() => import('./bootstrap').catch((err) => console.error(err))); + " + `); + expect(tree.read('myhostapp/src/app/app.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import * as React from 'react'; + + import NxWelcome from './nx-welcome'; + + import { Link, Route, Routes } from 'react-router-dom'; + + import { loadRemote } from '@module-federation/enhanced/runtime'; + + const Remote1 = React.lazy(() => loadRemote('remote1/Module') as any); + + export function App() { + return ( + +
    +
  • + Home +
  • + +
  • + Remote1 +
  • +
+ + } /> + + } /> + +
+ ); + } + + export default App; + " + `); + }); }); }); diff --git a/packages/react/src/generators/host/host.ts b/packages/react/src/generators/host/host.ts index 8dd7d9082b268..84ae83e851e3b 100644 --- a/packages/react/src/generators/host/host.ts +++ b/packages/react/src/generators/host/host.ts @@ -23,7 +23,10 @@ import { updateModuleFederationE2eProject } from './lib/update-module-federation import { NormalizedSchema, Schema } from './schema'; import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs'; import { isValidVariable } from '@nx/js'; -import { moduleFederationEnhancedVersion } from '../../utils/versions'; +import { + moduleFederationEnhancedVersion, + nxVersion, +} from '../../utils/versions'; import { ensureProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; @@ -147,8 +150,10 @@ export async function hostGenerator( const installTask = addDependenciesToPackageJson( host, - {}, - { '@module-federation/enhanced': moduleFederationEnhancedVersion } + { '@module-federation/enhanced': moduleFederationEnhancedVersion }, + { + '@nx/web': nxVersion, + } ); tasks.push(installTask); diff --git a/packages/react/src/generators/host/lib/add-module-federation-files.ts b/packages/react/src/generators/host/lib/add-module-federation-files.ts index a422875032c32..22dda0d73fee7 100644 --- a/packages/react/src/generators/host/lib/add-module-federation-files.ts +++ b/packages/react/src/generators/host/lib/add-module-federation-files.ts @@ -117,7 +117,10 @@ export function addModuleFederationFiles( pathToMFManifest, `{ ${defaultRemoteManifest - .map(({ name, port }) => `"${name}": "http://localhost:${port}"`) + .map( + ({ name, port }) => + `"${name}": "http://localhost:${port}/mf-manifest.json"` + ) .join(',\n')} }` ); From 564de350c673b93591e0fc56faebbebe91b7a99b Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 11:11:50 +0000 Subject: [PATCH 4/6] docs(react): update deprecation message --- packages/react/mf/dynamic-federation.ts | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/react/mf/dynamic-federation.ts b/packages/react/mf/dynamic-federation.ts index 5f5e60eeec7e2..bc1cfb19a6cb5 100644 --- a/packages/react/mf/dynamic-federation.ts +++ b/packages/react/mf/dynamic-federation.ts @@ -21,20 +21,84 @@ const remoteModuleMap = new Map(); const remoteContainerMap = new Map(); let initialSharingScopeCreated = false; +/** + * @deprecated Use Runtime Helpers from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + */ export function setRemoteUrlResolver( _resolveRemoteUrl: ResolveRemoteUrlFunction ) { resolveRemoteUrl = _resolveRemoteUrl; } +/** + * @deprecated Use init() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you have a remote app called `my-remote-app` and you want to use the `http://localhost:4201/mf-manifest.json` as the remote url, you should change it from: + * ```ts + * import { setRemoteDefinitions } from '@nx/react/mf'; + * + * setRemoteDefinitions({ + * 'my-remote-app': 'http://localhost:4201/mf-manifest.json' + * }); + * ``` + * to use init(): + * ```ts + * import { init } from '@module-federation/enhanced/runtime'; + * + * init({ + * name: 'host', + * remotes: [{ + * name: 'my-remote-app', + * entry: 'http://localhost:4201/mf-manifest.json' + * }] + * }); + * ``` + */ export function setRemoteDefinitions(definitions: Record) { remoteUrlDefinitions = definitions; } +/** + * @deprecated Use registerRemotes() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you set a remote app with `setRemoteDefinition` such as: + * ```ts + * import { setRemoteDefinition } from '@nx/react/mf'; + * + * setRemoteDefinition( + * 'my-remote-app', + * 'http://localhost:4201/mf-manifest.json' + * ); + * ``` + * change it to use registerRemotes(): + * ```ts + * import { registerRemotes } from '@module-federation/enhanced/runtime'; + * + * registerRemotes([ + * { + * name: 'my-remote-app', + * entry: 'http://localhost:4201/mf-manifest.json' + * } + * ]); + * ``` + */ export function setRemoteDefinition(remoteName: string, remoteUrl: string) { remoteUrlDefinitions[remoteName] = remoteUrl; } +/** + * @deprecated Use loadRemote() from '@module-federation/enhanced/runtime' instead. This will be removed in Nx 22. + * If you set a load a remote with `loadRemoteModule` such as: + * ```ts + * import { loadRemoteModule } from '@nx/react/mf'; + * + * loadRemoteModule('my-remote-app', './Module').then(m => m.RemoteEntryModule); + * ``` + * change it to use loadRemote(): + * ```ts + * import { loadRemote } from '@module-federation/enhanced/runtime'; + * + * loadRemote('my-remote-app/Module').then(m => m.RemoteEntryModule); + * ``` + */ export async function loadRemoteModule(remoteName: string, moduleName: string) { const remoteModuleKey = `${remoteName}:${moduleName}`; if (remoteModuleMap.has(remoteModuleKey)) { From 4b615ca84084d32e0d65a804269b03b3b55a9cf9 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 12:04:00 +0000 Subject: [PATCH 5/6] chore(react): fix snapshots --- .../__snapshots__/host.rspack.spec.ts.snap | 31 +++++++++---------- .../src/generators/host/host.rspack.spec.ts | 5 --- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap b/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap index c38adcb67d1f1..6395cdf489168 100644 --- a/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap +++ b/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap @@ -135,7 +135,7 @@ import {withModuleFederation, ModuleFederationConfig} from '@nx/rspack/module-fe import baseConfig from './module-federation.config'; const config: ModuleFederationConfig = { - ...baseConfig, + ...baseConfig, }; // Nx plugins for rspack to build config object from Nx options and context. @@ -152,21 +152,20 @@ exports[`hostGenerator bundler=rspack should generate host files and configs whe "import { ModuleFederationConfig } from '@nx/rspack/module-federation'; const config: ModuleFederationConfig = { - name: 'test', - /** - * To use a remote that does not exist in your current Nx Workspace - * You can use the tuple-syntax to define your remote - * - * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']] - * - * You _may_ need to add a \`remotes.d.ts\` file to your \`src/\` folder declaring the external remote for tsc, with the - * following content: - * - * declare module 'my-external-remote'; - * - */ - remotes: [ - + name: 'test', + /** + * To use a remote that does not exist in your current Nx Workspace + * You can use the tuple-syntax to define your remote + * + * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']] + * + * You _may_ need to add a \`remotes.d.ts\` file to your \`src/\` folder declaring the external remote for tsc, with the + * following content: + * + * declare module 'my-external-remote'; + * + */ + remotes: [ ], }; diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts index c15771daa2fb3..7aaec551dbf78 100644 --- a/packages/react/src/generators/host/host.rspack.spec.ts +++ b/packages/react/src/generators/host/host.rspack.spec.ts @@ -396,11 +396,8 @@ describe('hostGenerator', () => { expect(tree.read('myhostapp/src/app/app.tsx', 'utf-8')) .toMatchInlineSnapshot(` "import * as React from 'react'; - import NxWelcome from './nx-welcome'; - import { Link, Route, Routes } from 'react-router-dom'; - import { loadRemote } from '@module-federation/enhanced/runtime'; const Remote1 = React.lazy(() => loadRemote('remote1/Module') as any); @@ -412,14 +409,12 @@ describe('hostGenerator', () => {
  • Home
  • -
  • Remote1
  • } /> - } />
    From da806e9d629b38d6faab5c8c7115cd877a1c9982 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 15:24:16 +0000 Subject: [PATCH 6/6] chore(react): fix dyn fed test --- e2e/react/src/react-module-federation.rspack.test.ts | 6 ++++-- e2e/react/src/react-module-federation.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/e2e/react/src/react-module-federation.rspack.test.ts b/e2e/react/src/react-module-federation.rspack.test.ts index 3d301304994d5..d424db1735a73 100644 --- a/e2e/react/src/react-module-federation.rspack.test.ts +++ b/e2e/react/src/react-module-federation.rspack.test.ts @@ -1189,7 +1189,7 @@ describe('React Rspack Module Federation', () => { `${shell}/src/assets/module-federation.manifest.json`, (json) => { return { - [remote]: `http://localhost:${remotePort}`, + [remote]: `http://localhost:${remotePort}/mf-manifest.json`, }; } ); @@ -1198,7 +1198,9 @@ describe('React Rspack Module Federation', () => { `${shell}/src/assets/module-federation.manifest.json` ); expect(manifest[remote]).toBeDefined(); - expect(manifest[remote]).toEqual('http://localhost:4205'); + expect(manifest[remote]).toEqual( + 'http://localhost:4205/mf-manifest.json' + ); // update e2e updateFile( diff --git a/e2e/react/src/react-module-federation.test.ts b/e2e/react/src/react-module-federation.test.ts index 9b4cf2eebde0c..7f2d02c8ab9a4 100644 --- a/e2e/react/src/react-module-federation.test.ts +++ b/e2e/react/src/react-module-federation.test.ts @@ -1015,7 +1015,7 @@ describe('React Module Federation', () => { `${shell}/src/assets/module-federation.manifest.json`, (json) => { return { - [remote]: `http://localhost:${remotePort}`, + [remote]: `http://localhost:${remotePort}/mf-manifest.json`, }; } ); @@ -1024,7 +1024,9 @@ describe('React Module Federation', () => { `${shell}/src/assets/module-federation.manifest.json` ); expect(manifest[remote]).toBeDefined(); - expect(manifest[remote]).toEqual('http://localhost:4205'); + expect(manifest[remote]).toEqual( + 'http://localhost:4205/mf-manifest.json' + ); // update e2e updateFile(