Colum Ferry 43a20e2ecc
feat(angular): add support for rspack module federation (#31231)
## Current Behavior
We currently have no method for generating Angular Rspack Module
Federation applications

## Expected Behavior
Update the `host` and `remote` generators to support a `--bundler` flag
to allow users to select Rspack as their bundler method
2025-05-21 09:45:58 +01:00

267 lines
7.2 KiB
TypeScript

import {
applyAdditionalShared,
applySharedFunction,
getDependentPackagesForProject,
mapRemotes,
mapRemotesForSSR,
ModuleFederationConfig,
SharedLibraryConfig,
sharePackages,
shareWorkspaceLibraries,
} from '../../utils';
import {
createProjectGraphAsync,
ProjectGraph,
readCachedProjectGraph,
} from '@nx/devkit';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
export function applyDefaultEagerPackages(
sharedConfig: Record<string, SharedLibraryConfig>,
useRspack = false
) {
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
'@angular/localize',
'@angular/localize/init',
...(useRspack
? [
'@angular/core',
'@angular/core/primitives/signals',
'@angular/core/event-dispatch',
'@angular/core/rxjs-interop',
'@angular/common',
'@angular/common/http',
'@angular/platform-browser',
]
: []),
];
for (const pkg of DEFAULT_PACKAGES_TO_LOAD_EAGERLY) {
if (!sharedConfig[pkg]) {
continue;
}
sharedConfig[pkg] = { ...sharedConfig[pkg], eager: true };
}
}
export const DEFAULT_NPM_PACKAGES_TO_AVOID = [
'zone.js',
'@nx/angular/mf',
'@nrwl/angular/mf',
'@nx/angular-rspack',
];
export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
'@angular/core',
'@angular/animations',
'@angular/common',
];
export function getFunctionDeterminateRemoteUrl(
isServer: boolean = false,
useRspack = false
) {
const target = 'serve';
const remoteEntry = isServer
? 'server/remoteEntry.js'
: useRspack
? 'remoteEntry.js'
: 'remoteEntry.mjs';
return function (remote: string) {
const mappedStaticRemotesFromEnv = process.env
.NX_MF_DEV_SERVER_STATIC_REMOTES
? JSON.parse(process.env.NX_MF_DEV_SERVER_STATIC_REMOTES)
: undefined;
if (mappedStaticRemotesFromEnv && mappedStaticRemotesFromEnv[remote]) {
return `${mappedStaticRemotesFromEnv[remote]}/${remoteEntry}`;
}
let remoteConfiguration = null;
try {
remoteConfiguration = readCachedProjectConfiguration(remote);
} catch (e) {
throw new Error(
`Cannot find remote "${remote}". Check that the remote name is correct in your module federation config file.\n`
);
}
const serveTarget = remoteConfiguration?.targets?.[target];
if (!serveTarget) {
throw new Error(
`Cannot automatically determine URL of remote (${remote}). Looked for property "host" in the project's "serve" target.\n
You can also use the tuple syntax in your webpack config to configure your remotes. e.g. \`remotes: [['remote1', 'http://localhost:4201']]\``
);
}
const host =
serveTarget.options?.host ??
`http${serveTarget.options.ssl ? 's' : ''}://localhost`;
const port = serveTarget.options?.port ?? 4201;
return `${useRspack ? `${remote}@` : ''}${
host.endsWith('/') ? host.slice(0, -1) : host
}:${port}/${remoteEntry}`;
};
}
export async function getModuleFederationConfig(
mfConfig: ModuleFederationConfig,
options: {
isServer: boolean;
determineRemoteUrl?: (remote: string) => string;
} = { isServer: false }
) {
let projectGraph: ProjectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch (e) {
projectGraph = await createProjectGraphAsync();
}
if (!projectGraph.nodes[mfConfig.name]?.data) {
throw Error(
`Cannot find project "${mfConfig.name}". Check that the name is correct in module-federation.config.js`
);
}
const dependencies = getDependentPackagesForProject(
projectGraph,
mfConfig.name
);
if (mfConfig.shared) {
dependencies.workspaceLibraries = dependencies.workspaceLibraries.filter(
(lib) => mfConfig.shared(lib.importKey, {}) !== false
);
dependencies.npmPackages = dependencies.npmPackages.filter(
(pkg) => mfConfig.shared(pkg, {}) !== false
);
}
const sharedLibraries = shareWorkspaceLibraries(
dependencies.workspaceLibraries
);
const npmPackages = sharePackages(
Array.from(
new Set([
...DEFAULT_ANGULAR_PACKAGES_TO_SHARE,
...dependencies.npmPackages.filter(
(pkg) => !DEFAULT_NPM_PACKAGES_TO_AVOID.includes(pkg)
),
])
)
);
DEFAULT_NPM_PACKAGES_TO_AVOID.forEach((pkgName) => {
if (pkgName in npmPackages) {
delete npmPackages[pkgName];
}
});
const sharedDependencies = {
...sharedLibraries.getLibraries(
projectGraph.nodes[mfConfig.name].data.root
),
...npmPackages,
};
applyDefaultEagerPackages(sharedDependencies);
applySharedFunction(sharedDependencies, mfConfig.shared);
applyAdditionalShared(
sharedDependencies,
mfConfig.additionalShared,
projectGraph
);
const determineRemoteUrlFn =
options.determineRemoteUrl ||
getFunctionDeterminateRemoteUrl(options.isServer);
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
const mappedRemotes =
!mfConfig.remotes || mfConfig.remotes.length === 0
? {}
: mapRemotesFunction(mfConfig.remotes, 'mjs', determineRemoteUrlFn);
return { sharedLibraries, sharedDependencies, mappedRemotes };
}
export function getModuleFederationConfigSync(
mfConfig: ModuleFederationConfig,
options: {
isServer: boolean;
determineRemoteUrl?: (remote: string) => string;
} = { isServer: false },
useRspack = false
) {
const projectGraph: ProjectGraph = readCachedProjectGraph();
if (!projectGraph.nodes[mfConfig.name]?.data) {
throw Error(
`Cannot find project "${mfConfig.name}". Check that the name is correct in module-federation.config.js`
);
}
const dependencies = getDependentPackagesForProject(
projectGraph,
mfConfig.name
);
if (mfConfig.shared) {
dependencies.workspaceLibraries = dependencies.workspaceLibraries.filter(
(lib) => mfConfig.shared(lib.importKey, {}) !== false
);
dependencies.npmPackages = dependencies.npmPackages.filter(
(pkg) => mfConfig.shared(pkg, {}) !== false
);
}
const sharedLibraries = shareWorkspaceLibraries(
dependencies.workspaceLibraries
);
const npmPackages = sharePackages(
Array.from(
new Set([
...dependencies.npmPackages.filter(
(pkg) => !DEFAULT_NPM_PACKAGES_TO_AVOID.includes(pkg)
),
])
)
);
DEFAULT_NPM_PACKAGES_TO_AVOID.forEach((pkgName) => {
if (pkgName in npmPackages) {
delete npmPackages[pkgName];
}
});
const sharedDependencies = {
...sharedLibraries.getLibraries(
projectGraph.nodes[mfConfig.name].data.root
),
...npmPackages,
};
applyDefaultEagerPackages(sharedDependencies, useRspack);
applySharedFunction(sharedDependencies, mfConfig.shared);
applyAdditionalShared(
sharedDependencies,
mfConfig.additionalShared,
projectGraph
);
const determineRemoteUrlFn =
options.determineRemoteUrl ||
getFunctionDeterminateRemoteUrl(options.isServer, useRspack);
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
const mappedRemotes =
!mfConfig.remotes || mfConfig.remotes.length === 0
? {}
: mapRemotesFunction(
mfConfig.remotes,
useRspack ? 'js' : 'mjs',
determineRemoteUrlFn
);
return { sharedLibraries, sharedDependencies, mappedRemotes };
}