feat(module-federation): use module-federation runtime for dynamic federation (#28704)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
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
<!-- This is the behavior we should expect with the changes in this PR
-->
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
<img width="810" alt="image"
src="https://github.com/user-attachments/assets/6c6a9504-6d89-497b-9259-9272b3f47276">


## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-11-01 16:01:33 +00:00 committed by GitHub
parent ab8d77e719
commit 0f25b3c42e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 518 additions and 84 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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<string, string>;
/**
* @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<string, string>) {
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<string, unknown>();
let remoteContainerMap = new Map<string, unknown>();
/**
* @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<typeof import('my-remote-app/Module')>('my-remote-app/Module').then(m => m.RemoteEntryModule);
* ```
*/
export async function loadRemoteModule(remoteName: string, moduleName: string) {
const remoteModuleKey = `${remoteName}:${moduleName}`;
if (remoteModuleMap.has(remoteModuleKey)) {

View File

@ -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<string, string>) => 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<string, string>) => 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<typeof import('remote1/Module')>('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<typeof import('remote1/Module')>('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<typeof import('remote1/Module')>('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<typeof import('remote1/Module')>('remote1/Module').then(m => m!.RemoteEntryModule)
},
{
path: '',

View File

@ -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'
? usingLegacyDynamicFederation
? `loadRemoteModule('${options.appName.replace(
/-/g,
'_'
)}', './${routePathName}')`
: `import('${options.appName.replace(/-/g, '_')}/${routePathName}')`;
: `loadRemote<typeof import('${remoteModulePath}')>('${remoteModulePath}')`
: `import('${remoteModulePath}')`;
addRoute(
tree,
pathToHostRootRouting,
`{
path: '${options.appName}',
loadChildren: () => ${routeToAdd}.then(m => m.${exportedRemote})
loadChildren: () => ${routeToAdd}.then(m => m!.${exportedRemote})
}`
);

View File

@ -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<string, string>) => Object.entries(remotes).map(([name, entry]) => ({ name,entry})))
.then(remotes => init({name: '${options.appName}', remotes}))
.then(() => ${bootstrapImportCode});`;
tree.write(mainFilePath, fetchMFManifestCode);

View File

@ -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();
});

View File

@ -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,
}
);
}

View File

@ -21,20 +21,84 @@ const remoteModuleMap = new Map<string, unknown>();
const remoteContainerMap = new Map<string, unknown>();
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<string, string>) {
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<typeof import('my-remote-app/Module')>('my-remote-app/Module').then(m => m.RemoteEntryModule);
* ```
*/
export async function loadRemoteModule(remoteName: string, moduleName: string) {
const remoteModuleKey = `${remoteName}:${moduleName}`;
if (remoteModuleMap.has(remoteModuleKey)) {

View File

@ -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;
"
`;

View File

@ -4,16 +4,16 @@ 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'));
<%_ } _%>
<%_ }); _%>
<%_ } _%>

View File

@ -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<string, string>) =>
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));
import('./bootstrap').catch(err => console.error(err));
<%_ } _%>

View File

@ -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'));
<%_ }); _%>
<% } %>
<%_ 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 (
<React.Suspense fallback={null}>

View File

@ -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<string, string>) =>
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));
import('./bootstrap').catch(err => console.error(err));
<%_ } _%>

View File

@ -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 (
<React.Suspense fallback={null}>

View File

@ -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<string, string>) =>
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));
import('./bootstrap').catch(err => console.error(err));
<%_ } _%>

View File

@ -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<string, string>) =>
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 (
<React.Suspense fallback={null}>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/remote1">Remote1</Link>
</li>
</ul>
<Routes>
<Route path="/" element={<NxWelcome title="myhostapp" />} />
<Route path="/remote1" element={<Remote1 />} />
</Routes>
</React.Suspense>
);
}
export default App;
"
`);
});
});
});

View File

@ -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);

View File

@ -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')}
}`
);