Skip to content

Commit 6800b1e

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

File tree

13 files changed

+281
-0
lines changed

13 files changed

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

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+
}

0 commit comments

Comments
 (0)