From 0f25b3c42e57dc4077cac3c3d6eabbba74256ed0 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 1 Nov 2024 16:01:33 +0000 Subject: [PATCH] feat(module-federation): use module-federation runtime for dynamic federation (#28704) ## Current Behavior We currently have hombrewed support for Dynamic Module Federation. However, Module Federation 2.0 comes with more powerful helpers to handle dynamic federation. ## Expected Behavior For new host projects using dynamic federation, use the Module Federation Runtime. For existing hosts using Nx's dynamic federation, continue to use it when adding new remotes to it. Deprecate Nx's dynamic federation helpers with intended removal in Nx 22 ### Example Screenshot of Deprecation Message image ## Related Issue(s) Fixes # --- .../react-module-federation.rspack.test.ts | 6 +- e2e/react/src/react-module-federation.test.ts | 6 +- packages/angular/mf/mf.ts | 64 +++++++ .../__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 +- .../src/generators/setup-mf/setup-mf.ts | 5 +- packages/react/mf/dynamic-federation.ts | 64 +++++++ .../__snapshots__/host.rspack.spec.ts.snap | 177 ++++++++++++++++++ .../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 | 72 ++++++- packages/react/src/generators/host/host.ts | 11 +- .../host/lib/add-module-federation-files.ts | 5 +- 19 files changed, 518 insertions(+), 84 deletions(-) create mode 100644 packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap diff --git a/e2e/react/src/react-module-federation.rspack.test.ts b/e2e/react/src/react-module-federation.rspack.test.ts index 3d30130499..d424db1735 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 9b4cf2eebd..7f2d02c8ab 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( diff --git a/packages/angular/mf/mf.ts b/packages/angular/mf/mf.ts index 8184174b9e..eab3f1d8da 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,56 @@ export function setRemoteUrlResolver( 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; } +/** + * @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 ??= {}; remoteUrlDefinitions[remoteName] = remoteUrl; @@ -27,6 +76,21 @@ 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. + * 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}`; 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 8e908a8bc7..ff2d643486 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 9e8c57d752..c8cb6e2fa7 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 9dca073078..271e20fd9d 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 5490e9e781..c99bb98a6e 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(); }); diff --git a/packages/angular/src/generators/setup-mf/setup-mf.ts b/packages/angular/src/generators/setup-mf/setup-mf.ts index 5637e1cd95..bed23cc855 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/mf/dynamic-federation.ts b/packages/react/mf/dynamic-federation.ts index 5f5e60eeec..bc1cfb19a6 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)) { 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 0000000000..6395cdf489 --- /dev/null +++ b/packages/react/src/generators/host/__snapshots__/host.rspack.spec.ts.snap @@ -0,0 +1,177 @@ +// 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 2c6a48fdfc..f4789e0395 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 28b8cfb24d..52c51d1e1d 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 8252e92801..ab542c9315 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 f68313e1f9..52c51d1e1d 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 e0da5bf8c8..6d2c466714 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 f68313e1f9..52c51d1e1d 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 2925acf8b3..7aaec551db 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,67 @@ 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 8dd7d9082b..84ae83e851 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 a422875032..22dda0d73f 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')} }` );