Skip to content

Commit bb4abc1

Browse files
committed
feat(angular): add support for rspack module federation
1 parent 81ecb22 commit bb4abc1

File tree

13 files changed

+267
-0
lines changed

13 files changed

+267
-0
lines changed

docs/generated/packages/angular/generators/host.json

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
"x-priority": "important",
3838
"alias": "producers"
3939
},
40+
"bundler": {
41+
"type": "string",
42+
"description": "The bundler to use for the host application.",
43+
"default": "webpack",
44+
"enum": ["webpack", "rspack"]
45+
},
4046
"dynamic": {
4147
"type": "boolean",
4248
"description": "Should the host application use dynamic federation?",

docs/generated/packages/angular/generators/remote.json

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
"type": "number",
4343
"description": "The port on which this app should be served."
4444
},
45+
"bundler": {
46+
"type": "string",
47+
"description": "The bundler to use for the remote application.",
48+
"default": "webpack",
49+
"enum": ["webpack", "rspack"]
50+
},
4551
"style": {
4652
"description": "The file extension to be used for style files.",
4753
"type": "string",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { names } from '@nx/devkit';
2+
import {
3+
checkFilesExist,
4+
cleanupProject,
5+
killPorts,
6+
killProcessAndPorts,
7+
newProject,
8+
readJson,
9+
runCLI,
10+
runCommandUntil,
11+
runE2ETests,
12+
uniq,
13+
updateFile,
14+
updateJson,
15+
} from '@nx/e2e/utils';
16+
import { join } from 'path';
17+
18+
describe('Angular Module Federation', () => {
19+
let proj: string;
20+
let oldVerboseLoggingValue: string;
21+
22+
beforeAll(() => {
23+
proj = newProject({ packages: ['@nx/angular'] });
24+
oldVerboseLoggingValue = process.env.NX_E2E_VERBOSE_LOGGING;
25+
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
26+
});
27+
afterAll(() => {
28+
cleanupProject();
29+
process.env.NX_E2E_VERBOSE_LOGGING = oldVerboseLoggingValue;
30+
});
31+
32+
it('should generate valid host and remote apps', async () => {
33+
const hostApp = uniq('app');
34+
const remoteApp1 = uniq('remote');
35+
const sharedLib = uniq('shared-lib');
36+
const wildcardLib = uniq('wildcard-lib');
37+
const secondaryEntry = uniq('secondary');
38+
const hostPort = 4300;
39+
const remotePort = 4301;
40+
41+
// generate host app
42+
runCLI(
43+
`generate @nx/angular:host ${hostApp} --style=css --bundler=rspack --no-standalone --no-interactive`
44+
);
45+
// generate remote app
46+
runCLI(
47+
`generate @nx/angular:remote ${remoteApp1} --host=${hostApp} --bundler=rspack --port=${remotePort} --style=css --no-standalone --no-interactive`
48+
);
49+
50+
// check files are generated without the layout directory ("apps/")
51+
checkFilesExist(
52+
`${hostApp}/src/app/app.module.ts`,
53+
`${remoteApp1}/src/app/app.module.ts`
54+
);
55+
56+
// check default generated host is built successfully
57+
const buildOutput = runCLI(`build ${hostApp}`);
58+
expect(buildOutput).toContain('Successfully ran target build');
59+
60+
// generate a shared lib with a seconary entry point
61+
runCLI(
62+
`generate @nx/angular:library ${sharedLib} --buildable --no-standalone --no-interactive`
63+
);
64+
runCLI(
65+
`generate @nx/angular:library-secondary-entry-point --library=${sharedLib} --name=${secondaryEntry} --no-interactive`
66+
);
67+
68+
// Add a library that will be accessed via a wildcard in tspath mappings
69+
runCLI(
70+
`generate @nx/angular:library ${wildcardLib} --buildable --no-standalone --no-interactive`
71+
);
72+
73+
updateJson('tsconfig.base.json', (json) => {
74+
delete json.compilerOptions.paths[`@${proj}/${wildcardLib}`];
75+
json.compilerOptions.paths[`@${proj}/${wildcardLib}/*`] = [
76+
`${wildcardLib}/src/lib/*`,
77+
];
78+
return json;
79+
});
80+
81+
// update host & remote files to use shared library
82+
updateFile(
83+
`${hostApp}/src/app/app.module.ts`,
84+
`import { NgModule } from '@angular/core';
85+
import { BrowserModule } from '@angular/platform-browser';
86+
import { ${
87+
names(wildcardLib).className
88+
}Module } from '@${proj}/${wildcardLib}/${
89+
names(secondaryEntry).fileName
90+
}.module';
91+
import { ${
92+
names(sharedLib).className
93+
}Module } from '@${proj}/${sharedLib}';
94+
import { ${
95+
names(secondaryEntry).className
96+
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
97+
import { AppComponent } from './app.component';
98+
import { NxWelcomeComponent } from './nx-welcome.component';
99+
import { RouterModule } from '@angular/router';
100+
101+
@NgModule({
102+
declarations: [AppComponent, NxWelcomeComponent],
103+
imports: [
104+
BrowserModule,
105+
${names(sharedLib).className}Module,
106+
${names(wildcardLib).className}Module,
107+
RouterModule.forRoot(
108+
[
109+
{
110+
path: '${remoteApp1}',
111+
loadChildren: () =>
112+
import('${remoteApp1}/Module').then(
113+
(m) => m.RemoteEntryModule
114+
),
115+
},
116+
],
117+
{ initialNavigation: 'enabledBlocking' }
118+
),
119+
],
120+
providers: [],
121+
bootstrap: [AppComponent],
122+
})
123+
export class AppModule {}
124+
`
125+
);
126+
updateFile(
127+
`${remoteApp1}/src/app/remote-entry/entry.module.ts`,
128+
`import { NgModule } from '@angular/core';
129+
import { CommonModule } from '@angular/common';
130+
import { RouterModule } from '@angular/router';
131+
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
132+
import { ${
133+
names(secondaryEntry).className
134+
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
135+
import { RemoteEntryComponent } from './entry.component';
136+
import { NxWelcomeComponent } from './nx-welcome.component';
137+
138+
@NgModule({
139+
declarations: [RemoteEntryComponent, NxWelcomeComponent],
140+
imports: [
141+
CommonModule,
142+
${names(sharedLib).className}Module,
143+
RouterModule.forChild([
144+
{
145+
path: '',
146+
component: RemoteEntryComponent,
147+
},
148+
]),
149+
],
150+
providers: [],
151+
})
152+
export class RemoteEntryModule {}
153+
`
154+
);
155+
156+
const processSwc = await runCommandUntil(
157+
`serve ${remoteApp1}`,
158+
(output) =>
159+
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
160+
output.includes(`browser compiled`)
161+
);
162+
await killProcessAndPorts(processSwc.pid, remotePort);
163+
}, 20_000_000);
164+
});

packages/angular/src/generators/convert-to-rspack/convert-to-rspack.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
angularRspackVersion,
1818
nxVersion,
1919
tsNodeVersion,
20+
webpackMergeVersion,
2021
} from '../../utils/versions';
2122
import { createConfig } from './lib/create-config';
2223
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
@@ -506,6 +507,7 @@ export async function convertToRspack(
506507
{},
507508
{
508509
'@nx/angular-rspack': angularRspackVersion,
510+
'webpack-merge': webpackMergeVersion,
509511
'ts-node': tsNodeVersion,
510512
}
511513
);

packages/angular/src/generators/host/host.ts

+26
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import {
22
formatFiles,
33
getProjects,
44
joinPathFragments,
5+
readProjectConfiguration,
56
runTasksInSerial,
67
Tree,
8+
updateProjectConfiguration,
79
} from '@nx/devkit';
810
import {
911
determineProjectNameAndRootOptions,
@@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
1820
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
1921
import { updateSsrSetup } from './lib';
2022
import type { Schema } from './schema';
23+
import { assertRspackIsCSR } from '../utils/assert-mf-utils';
24+
import convertToRspack from '../convert-to-rspack/convert-to-rspack';
2125

2226
export async function host(tree: Tree, schema: Schema) {
2327
assertNotUsingTsSolutionSetup(tree, 'angular', 'host');
28+
// TODO: Replace with Rspack when confidence is high enough
29+
schema.bundler ??= 'webpack';
30+
const isRspack = schema.bundler === 'rspack';
31+
assertRspackIsCSR(
32+
schema.bundler,
33+
schema.ssr ?? false,
34+
schema.serverRouting ?? false
35+
);
2436

2537
const { typescriptConfiguration = true, ...options }: Schema = schema;
2638
options.standalone = options.standalone ?? true;
@@ -119,6 +131,20 @@ export async function host(tree: Tree, schema: Schema) {
119131

120132
addMfEnvToTargetDefaultInputs(tree);
121133

134+
if (isRspack) {
135+
await convertToRspack(tree, {
136+
project: hostProjectName,
137+
skipInstall: options.skipPackageJson,
138+
skipFormat: true,
139+
});
140+
}
141+
142+
const project = readProjectConfiguration(tree, hostProjectName);
143+
project.targets.serve ??= {};
144+
project.targets.serve.options ??= {};
145+
project.targets.serve.options.port = 4200;
146+
updateProjectConfiguration(tree, hostProjectName, project);
147+
122148
if (!options.skipFormat) {
123149
await formatFiles(tree);
124150
}

packages/angular/src/generators/host/schema.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
55
export interface Schema {
66
directory: string;
77
name?: string;
8+
bundler?: 'webpack' | 'rspack';
89
remotes?: string[];
910
dynamic?: boolean;
1011
setParserOptionsProject?: boolean;

packages/angular/src/generators/host/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
"x-priority": "important",
3838
"alias": "producers"
3939
},
40+
"bundler": {
41+
"type": "string",
42+
"description": "The bundler to use for the host application.",
43+
"default": "webpack",
44+
"enum": ["webpack", "rspack"]
45+
},
4046
"dynamic": {
4147
"type": "boolean",
4248
"description": "Should the host application use dynamic federation?",

packages/angular/src/generators/remote/remote.ts

+30
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import {
22
addDependenciesToPackageJson,
33
formatFiles,
44
getProjects,
5+
readProjectConfiguration,
56
runTasksInSerial,
67
stripIndents,
78
Tree,
9+
updateProjectConfiguration,
810
} from '@nx/devkit';
911
import {
1012
determineProjectNameAndRootOptions,
@@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
1820
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
1921
import { findNextAvailablePort, updateSsrSetup } from './lib';
2022
import type { Schema } from './schema';
23+
import { assertRspackIsCSR } from '../utils/assert-mf-utils';
24+
import convertToRspack from '../convert-to-rspack/convert-to-rspack';
2125

2226
export async function remote(tree: Tree, schema: Schema) {
2327
assertNotUsingTsSolutionSetup(tree, 'angular', 'remote');
28+
// TODO: Replace with Rspack when confidence is high enough
29+
schema.bundler ??= 'webpack';
30+
const isRspack = schema.bundler === 'rspack';
31+
assertRspackIsCSR(
32+
schema.bundler,
33+
schema.ssr ?? false,
34+
schema.serverRouting ?? false
35+
);
2436

2537
const { typescriptConfiguration = true, ...options }: Schema = schema;
2638
options.standalone = options.standalone ?? true;
@@ -105,6 +117,24 @@ export async function remote(tree: Tree, schema: Schema) {
105117

106118
addMfEnvToTargetDefaultInputs(tree);
107119

120+
if (isRspack) {
121+
await convertToRspack(tree, {
122+
project: remoteProjectName,
123+
skipInstall: options.skipPackageJson,
124+
skipFormat: true,
125+
});
126+
}
127+
128+
const project = readProjectConfiguration(tree, remoteProjectName);
129+
project.targets.serve ??= {};
130+
project.targets.serve.options ??= {};
131+
if (options.host) {
132+
project.targets.serve.dependsOn ??= [];
133+
project.targets.serve.dependsOn.push(`${options.host}:serve`);
134+
}
135+
project.targets.serve.options.port = port;
136+
updateProjectConfiguration(tree, remoteProjectName, project);
137+
108138
if (!options.skipFormat) {
109139
await formatFiles(tree);
110140
}

packages/angular/src/generators/remote/schema.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
55
export interface Schema {
66
directory: string;
77
name?: string;
8+
bundler?: 'webpack' | 'rspack';
89
host?: string;
910
port?: number;
1011
setParserOptionsProject?: boolean;

packages/angular/src/generators/remote/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
"type": "number",
4343
"description": "The port on which this app should be served."
4444
},
45+
"bundler": {
46+
"type": "string",
47+
"description": "The bundler to use for the remote application.",
48+
"default": "webpack",
49+
"enum": ["webpack", "rspack"]
50+
},
4551
"style": {
4652
"description": "The file extension to be used for style files.",
4753
"type": "string",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function assertRspackIsCSR(
2+
bundler: 'webpack' | 'rspack',
3+
ssr: boolean,
4+
serverRouting: boolean
5+
) {
6+
if (bundler === 'rspack' && serverRouting) {
7+
throw new Error(
8+
'Server Routing is not currently supported for Angular Rspack Module Federation. Please use webpack instead.'
9+
);
10+
}
11+
if (bundler === 'rspack' && ssr) {
12+
throw new Error(
13+
'SSR is not currently supported for Angular Rspack Module Federation. Please use webpack instead.'
14+
);
15+
}
16+
}

packages/angular/src/utils/backward-compatible-versions.ts

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const backwardCompatibleVersions: VersionMap = {
5151
typesNodeVersion: '18.16.9',
5252
jasmineMarblesVersion: '^0.9.2',
5353
jsoncEslintParserVersion: '^2.1.0',
54+
webpackMergeVersion: '^5.8.0',
5455
},
5556
angularV18: {
5657
angularVersion: '~18.2.0',
@@ -80,5 +81,6 @@ export const backwardCompatibleVersions: VersionMap = {
8081
typesNodeVersion: '18.16.9',
8182
jasmineMarblesVersion: '^0.9.2',
8283
jsoncEslintParserVersion: '^2.1.0',
84+
webpackMergeVersion: '^5.8.0',
8385
},
8486
};

packages/angular/src/utils/versions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const typesExpressVersion = '^4.17.21';
1717
export const browserSyncVersion = '^3.0.0';
1818
export const moduleFederationNodeVersion = '^2.6.26';
1919
export const moduleFederationEnhancedVersion = '^0.9.0';
20+
export const webpackMergeVersion = '^5.8.0';
2021

2122
export const angularEslintVersion = '^19.2.0';
2223
export const typescriptEslintVersion = '^7.16.0';

0 commit comments

Comments
 (0)