Skip to content

feat(angular): add support for rspack module federation #31231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/generated/packages/angular/generators/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
"x-priority": "important",
"alias": "producers"
},
"bundler": {
"type": "string",
"description": "The bundler to use for the host application.",
"default": "webpack",
"enum": ["webpack", "rspack"]
},
"dynamic": {
"type": "boolean",
"description": "Should the host application use dynamic federation?",
Expand Down
6 changes: 6 additions & 0 deletions docs/generated/packages/angular/generators/remote.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
"type": "number",
"description": "The port on which this app should be served."
},
"bundler": {
"type": "string",
"description": "The bundler to use for the remote application.",
"default": "webpack",
"enum": ["webpack", "rspack"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
Expand Down
178 changes: 178 additions & 0 deletions e2e/angular/src/module-federation.rspack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { names } from '@nx/devkit';
import {
checkFilesExist,
cleanupProject,
killPorts,
killProcessAndPorts,
newProject,
readFile,
readJson,
runCLI,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { join } from 'path';

describe('Angular Module Federation', () => {
let proj: string;
let oldVerboseLoggingValue: string;

beforeAll(() => {
proj = newProject({ packages: ['@nx/angular'] });
oldVerboseLoggingValue = process.env.NX_E2E_VERBOSE_LOGGING;
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
});
afterAll(() => {
cleanupProject();
process.env.NX_E2E_VERBOSE_LOGGING = oldVerboseLoggingValue;
});

it('should generate valid host and remote apps', async () => {
const hostApp = uniq('app');
const remoteApp1 = uniq('remote');
const sharedLib = uniq('shared-lib');
const wildcardLib = uniq('wildcard-lib');
const secondaryEntry = uniq('secondary');
const hostPort = 4300;
const remotePort = 4301;

// generate host app
runCLI(
`generate @nx/angular:host ${hostApp} --style=css --bundler=rspack --no-standalone --no-interactive`
);
let rspackConfigFileContents = readFile(join(hostApp, 'rspack.config.ts'));
let updatedConfigFileContents = rspackConfigFileContents.replace(
`maximumError: '1mb'`,
`maximumError: '11mb'`
);
updateFile(join(hostApp, 'rspack.config.ts'), updatedConfigFileContents);

// generate remote app
runCLI(
`generate @nx/angular:remote ${remoteApp1} --host=${hostApp} --bundler=rspack --port=${remotePort} --style=css --no-standalone --no-interactive`
);
rspackConfigFileContents = readFile(join(remoteApp1, 'rspack.config.ts'));
updatedConfigFileContents = rspackConfigFileContents.replace(
`maximumError: '1mb'`,
`maximumError: '11mb'`
);
updateFile(join(remoteApp1, 'rspack.config.ts'), updatedConfigFileContents);

// check files are generated without the layout directory ("apps/")
checkFilesExist(
`${hostApp}/src/app/app.module.ts`,
`${remoteApp1}/src/app/app.module.ts`
);

// check default generated host is built successfully
const buildOutput = runCLI(`build ${hostApp}`);
expect(buildOutput).toContain('Successfully ran target build');

// generate a shared lib with a seconary entry point
runCLI(
`generate @nx/angular:library ${sharedLib} --buildable --no-standalone --no-interactive`
);
runCLI(
`generate @nx/angular:library-secondary-entry-point --library=${sharedLib} --name=${secondaryEntry} --no-interactive`
);

// Add a library that will be accessed via a wildcard in tspath mappings
runCLI(
`generate @nx/angular:library ${wildcardLib} --buildable --no-standalone --no-interactive`
);

updateJson('tsconfig.base.json', (json) => {
delete json.compilerOptions.paths[`@${proj}/${wildcardLib}`];
json.compilerOptions.paths[`@${proj}/${wildcardLib}/*`] = [
`${wildcardLib}/src/lib/*`,
];
return json;
});

// update host & remote files to use shared library
updateFile(
`${hostApp}/src/app/app.module.ts`,
`import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ${
names(wildcardLib).className
}Module } from '@${proj}/${wildcardLib}/${
names(secondaryEntry).fileName
}.module';
import { ${
names(sharedLib).className
}Module } from '@${proj}/${sharedLib}';
import { ${
names(secondaryEntry).className
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
import { RouterModule } from '@angular/router';

@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [
BrowserModule,
${names(sharedLib).className}Module,
${names(wildcardLib).className}Module,
RouterModule.forRoot(
[
{
path: '${remoteApp1}',
loadChildren: () =>
import('${remoteApp1}/Module').then(
(m) => m.RemoteEntryModule
),
},
],
{ initialNavigation: 'enabledBlocking' }
),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
`
);
updateFile(
`${remoteApp1}/src/app/remote-entry/entry.module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
import { ${
names(secondaryEntry).className
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
import { RemoteEntryComponent } from './entry.component';
import { NxWelcomeComponent } from './nx-welcome.component';

@NgModule({
declarations: [RemoteEntryComponent, NxWelcomeComponent],
imports: [
CommonModule,
${names(sharedLib).className}Module,
RouterModule.forChild([
{
path: '',
component: RemoteEntryComponent,
},
]),
],
providers: [],
})
export class RemoteEntryModule {}
`
);

const processSwc = await runCommandUntil(
`serve ${remoteApp1}`,
(output) =>
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
output.includes(`Build at:`)
);
await killProcessAndPorts(processSwc.pid, remotePort);
}, 20_000_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
angularRspackVersion,
nxVersion,
tsNodeVersion,
webpackMergeVersion,
} from '../../utils/versions';
import { createConfig } from './lib/create-config';
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
Expand Down Expand Up @@ -47,12 +48,7 @@ const RENAMED_OPTIONS = {

const DEFAULT_PORT = 4200;

const REMOVED_OPTIONS = [
'buildOptimizer',
'buildTarget',
'browserTarget',
'publicHost',
];
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];

function normalizeFromProjectRoot(
tree: Tree,
Expand Down Expand Up @@ -506,6 +502,7 @@ export async function convertToRspack(
{},
{
'@nx/angular-rspack': angularRspackVersion,
'webpack-merge': webpackMergeVersion,
'ts-node': tsNodeVersion,
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('convertconvertWebpackConfigToUseNxModuleFederationPlugin', () => {
// ASSERT
expect(newWebpackConfigContents).toMatchInlineSnapshot(`
"
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/angular';
import config from './module-federation.config';


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function convertWebpackConfigToUseNxModuleFederationPlugin(
newWebpackConfigContents = `${webpackConfigContents.slice(
0,
withModuleFederationImportNode.getStart()
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';${webpackConfigContents.slice(
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/angular';${webpackConfigContents.slice(
withModuleFederationImportNode.getEnd()
)}`;

Expand Down
30 changes: 29 additions & 1 deletion packages/angular/src/generators/host/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {
formatFiles,
getProjects,
joinPathFragments,
readProjectConfiguration,
runTasksInSerial,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import {
determineProjectNameAndRootOptions,
Expand All @@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
import { updateSsrSetup } from './lib';
import type { Schema } from './schema';
import { assertRspackIsCSR } from '../utils/assert-mf-utils';
import convertToRspack from '../convert-to-rspack/convert-to-rspack';

export async function host(tree: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(tree, 'angular', 'host');
// TODO: Replace with Rspack when confidence is high enough
schema.bundler ??= 'webpack';
const isRspack = schema.bundler === 'rspack';
assertRspackIsCSR(
schema.bundler,
schema.ssr ?? false,
schema.serverRouting ?? false
);

const { typescriptConfiguration = true, ...options }: Schema = schema;
options.standalone = options.standalone ?? true;
Expand Down Expand Up @@ -100,7 +112,8 @@ export async function host(tree: Tree, schema: Schema) {
installTasks.push(ssrInstallTask);
}

for (const remote of remotesToGenerate) {
for (let i = 0; i < remotesToGenerate.length; i++) {
const remote = remotesToGenerate[i];
const remoteDirectory = options.directory
? joinPathFragments(options.directory, '..', remote)
: appRoot === '.'
Expand All @@ -111,6 +124,7 @@ export async function host(tree: Tree, schema: Schema) {
name: remote,
directory: remoteDirectory,
host: hostProjectName,
port: isRspack ? 4200 + i + 1 : undefined,
skipFormat: true,
standalone: options.standalone,
typescriptConfiguration,
Expand All @@ -119,6 +133,20 @@ export async function host(tree: Tree, schema: Schema) {

addMfEnvToTargetDefaultInputs(tree);

if (isRspack) {
await convertToRspack(tree, {
project: hostProjectName,
skipInstall: options.skipPackageJson,
skipFormat: true,
});
}

const project = readProjectConfiguration(tree, hostProjectName);
project.targets.serve ??= {};
project.targets.serve.options ??= {};
project.targets.serve.options.port = 4200;
updateProjectConfiguration(tree, hostProjectName, project);

if (!options.skipFormat) {
await formatFiles(tree);
}
Expand Down
1 change: 1 addition & 0 deletions packages/angular/src/generators/host/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
export interface Schema {
directory: string;
name?: string;
bundler?: 'webpack' | 'rspack';
remotes?: string[];
dynamic?: boolean;
setParserOptionsProject?: boolean;
Expand Down
6 changes: 6 additions & 0 deletions packages/angular/src/generators/host/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
"x-priority": "important",
"alias": "producers"
},
"bundler": {
"type": "string",
"description": "The bundler to use for the host application.",
"default": "webpack",
"enum": ["webpack", "rspack"]
},
"dynamic": {
"type": "boolean",
"description": "Should the host application use dynamic federation?",
Expand Down
Loading
Loading