Skip to content
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: 3 additions & 3 deletions libs/mf-runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@angular-architects/module-federation-runtime",
"license": "MIT",
"version": "21.2.2",
"version": "22.0.0",
"peerDependencies": {
"@angular/common": "^21.2.0",
"@angular/core": "^21.2.0",
"@angular/common": "^22.0.0",
"@angular/core": "^22.0.0",
"@module-federation/enhanced": "^2.2.2",
"@module-federation/runtime-core": "^2.2.2"
},
Expand Down
2 changes: 1 addition & 1 deletion libs/mf-runtime/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"target": "es2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
// "strict": true,
"strict": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
Expand Down
2 changes: 1 addition & 1 deletion libs/mf-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@angular-architects/module-federation-tools",
"version": "21.2.2",
"version": "22.0.0",
"license": "MIT",
"peerDependencies": {},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion libs/mf-tools/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"target": "es2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
// "strict": true,
"strict": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
Expand Down
12 changes: 7 additions & 5 deletions libs/mf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Module Federation allows loading separately compiled and deployed code (like mic

## Supporting Several Technologies

✅ Supports webpack, rsbuild (experimental, nextgen), esbuild
✅ Supports webpack, rspack (experimental, nextgen), esbuild

✅ Supports Module Federation and Native Federation

Expand Down Expand Up @@ -61,11 +61,11 @@ We are going to provide at least one major per Angular major to keep track with

Beginning with Angular 13, we had to add some changes to adjust to the Angular CLI. Please see the next section for this.

## webpack, rsbuild, and esbuild
## webpack, rspack, and esbuild

Since version 19, the plugin's ng-add schematic asks whether you want to use the traditional Webpack-based builder, the (currently experimental, fast, next-generation) rsbuild builder, or esbuild (fast, Angular CLI's new default).
Since version 19, the plugin's ng-add schematic asks whether you want to use the traditional Webpack-based builder, the (currently experimental, fast, next-generation) rspack builder, or esbuild (fast, Angular CLI's new default).

The first two are supported via Module Federation. For the rsbuild integration, we are using Colum Ferry's awesome community project, @ng-rsbuild/plugin-angular. So, all credit for making rsbuild work with Angular goes to Colum.
The first two are supported via Module Federation. For the rspack integration, we build on `@nx/angular-rspack` — originally Colum Ferry's awesome community project (`@ng-rsbuild/plugin-angular` / `@ng-rspack/build`), now maintained by the Nx team. So, all credit for making rspack work with Angular goes to Colum and the Nx team.

The esbuild integration is technically a wrapper around the Angular CLI's new default builder, the ApplicationBuilder. It is powered by Native Federation, our bundler-agnostic implementation based on web standards like ECMAScript modules and Import Maps.

Expand All @@ -77,7 +77,9 @@ This library supports `ng update`:
ng update @angular-architects/module-federation
```

If you update by hand (e. g. via `npm install`), make sure you also install a respective version of ngx-build-plus (version 15 for Angular 15, version 14 for Angular 14, version 13 for Angular 13, etc.)
If you update by hand (e. g. via `npm install`), make sure you also install a respective version of `@angular-builders/custom-webpack` matching your Angular major (version 22 for Angular 22, version 21 for Angular 21, etc.).

> Note: Up to and including Angular 21, this package used `ngx-build-plus` to inject the webpack config. As of Angular 22 it uses `@angular-builders/custom-webpack`. Running `ng update @angular-architects/module-federation` migrates existing workspaces automatically; if you update by hand, remove `ngx-build-plus` and switch your `angular.json` builders to `@angular-builders/custom-webpack:*`.

## Upgrade from Angular 12 or lower

Expand Down
5 changes: 0 additions & 5 deletions libs/mf/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@
"factory": "./src/schematics/remove/schematic",
"schema": "./src/schematics/remove/schema.json",
"description": "Removes Module Federation"
},
"patch": {
"factory": "./src/schematics/patch/schematic",
"description": "Patching Angular for rsbuild integration while experimental. Will be removed when stable!",
"private": true
}
}
}
6 changes: 3 additions & 3 deletions libs/mf/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@angular-architects/module-federation",
"version": "21.2.2",
"version": "22.0.0",
"license": "MIT",
"repository": {
"type": "GitHub",
Expand All @@ -17,7 +17,7 @@
"schematics": "./collection.json",
"builders": "./builders.json",
"dependencies": {
"@angular-architects/module-federation-runtime": "~21.2.2",
"@angular-architects/module-federation-runtime": "~22.0.0",
"word-wrap": "^1.2.5",
"callsite": "^1.0.0",
"node-fetch": "^3.3.2",
Expand All @@ -28,7 +28,7 @@
"migrations": "./migrations.json",
"package-group": [
"@angular-architects/module-federation-tools",
"ngx-build-plus"
"@angular-builders/custom-webpack"
]
}
}
2 changes: 1 addition & 1 deletion libs/mf/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"output": "."
},
{
"input": ".",
"input": "./libs/mf",
"glob": "migrations.json",
"output": "."
},
Expand Down
1 change: 0 additions & 1 deletion libs/mf/src/rspack/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './plugin-script-module';
export * from './with-federation';
24 changes: 0 additions & 24 deletions libs/mf/src/rspack/plugin-script-module.ts

This file was deleted.

185 changes: 101 additions & 84 deletions libs/mf/src/rspack/with-federation.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,133 @@
import { ModuleFederationConfig, RsbuildConfig } from '@rsbuild/core';
import { pluginScriptModule } from './plugin-script-module';
import { applySkipList, normalizeSkipList, SkipList } from '../utils/skip-list';
import type { Configuration } from '@rspack/core';
import { Shared } from '@rspack/core/dist/sharing/SharePlugin';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { applySkipList, normalizeSkipList, SkipList } from '../utils/skip-list';
import { findRootTsConfigJson, SharedMappings } from '../webpack';

export type FederationOptions = {
name?: string;
exposes?: Record<string, string>;
remotes?: Record<string, string>;
shared?: Shared;
};

export type FederationConfig = {
options: FederationOptions;
skip?: SkipList;
};

export type FederationOptions = Omit<
ModuleFederationConfig['options'],
'name'
> & {
name?: string;
};

export function withFederation(config: FederationConfig) {
//
// This provides partial application for better DX,
// as it allows to split the config into a file with
// rsbuild and an other one with federation settings
// `@nx/angular-rspack`'s `createConfig` resolves to a `Configuration[]`
// (browser config, plus a server config when SSR is enabled). We apply
// Module Federation to the browser config and leave any server config
// untouched, so this composes directly with `createConfig(...).then(...)`.
//
return (rsbuildConfig: RsbuildConfig) => {
return applyFederation(rsbuildConfig, config);
};
return (configs: Configuration[]): Configuration[] =>
configs.map((rspackConfig) =>
isServerConfig(rspackConfig)
? rspackConfig
: applyFederation(rspackConfig, config),
);
}

export function applyFederation(
rsbuildConfig: RsbuildConfig,
rspackConfig: Configuration,
federationConfig: FederationConfig,
): RsbuildConfig {
): Configuration {
const { skip, ...mfConfig } = federationConfig;
const normalizedSkip = normalizeSkipList(skip);

const mappings = new SharedMappings();
mappings.register(findRootTsConfigJson());

const shared = (mfConfig.options.shared ?? {}) as Shared;
const sharedWithLibs = {
const sharedWithLibs: Shared = {
...mappings.getDescriptors(),
...shared,
};
const filteredShared = applySkipList(normalizedSkip, sharedWithLibs);

const filteredSkipList = applySkipList(normalizedSkip, sharedWithLibs);

mfConfig.options.shared = filteredSkipList as Shared;
//
// Angular output produced by `@nx/angular-rspack` is an ES module (it relies
// on `import.meta`, and its entry scripts are emitted as `type="module"`).
// The federation container therefore has to be an ES module as well —
// otherwise the runtime loads `remoteEntry.js` as a classic script and
// `import.meta` throws (`Cannot use 'import.meta' outside a module`).
//
rspackConfig.experiments = {
...rspackConfig.experiments,
outputModule: true,
};
rspackConfig.output = {
...rspackConfig.output,
uniqueName: mfConfig.options.name || undefined,
publicPath: 'auto',
module: true,
library: { type: 'module' },
chunkFormat: 'module',
chunkLoading: 'import',
workerChunkLoading: 'import',
wasmLoading: 'fetch',
};
rspackConfig.optimization = {
...rspackConfig.optimization,
runtimeChunk: 'single',
};

const config: RsbuildConfig = {
...rsbuildConfig,
resolve: {
...rsbuildConfig.resolve,
alias: {
...rsbuildConfig.resolve?.alias,
...mappings.getAliases(),
},
rspackConfig.resolve = {
...rspackConfig.resolve,
alias: {
...rspackConfig.resolve?.alias,
...mappings.getAliases(),
},
dev: {
...rsbuildConfig.dev,
// chunkFormat: 'module' does not work with hmr yet
hmr: false,
},
server: {
...rsbuildConfig.server,
cors: true,
},
plugins: [
...(rsbuildConfig.plugins ?? []),
// mappings.getPlugin(),
};

pluginScriptModule(),
],
tools: {
rspack: {
experiments: {
outputModule: true,
},
plugins: [mappings.getPlugin()],
output: {
uniqueName: mfConfig.options.name,
publicPath: 'auto',
chunkFormat: 'module',
chunkLoading: 'import',
workerChunkLoading: 'import',
wasmLoading: 'fetch',
library: { type: 'module' },
module: true,
},
optimization: {
runtimeChunk: 'single',
},
},
},
moduleFederation: {
...mfConfig,
options: {
//
// Shells use an empty name by default
// Alternative: Specifiying the *same* name
// in initFederation (runtime) and
// withFederation (build time)
//
name: '',
...mfConfig.options,
library: {
...mfConfig.options.library,
type: 'module',
},
remoteType: 'module',
filename: 'remoteEntry.js',
},
rspackConfig.plugins ??= [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rspackConfig.plugins.push(mappings.getPlugin() as any);
rspackConfig.plugins.push(
new ModuleFederationPlugin({
name: mfConfig.options.name || 'host',
filename: 'remoteEntry.js',
exposes: mfConfig.options.exposes,
remotes: mfConfig.options.remotes,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
shared: filteredShared as any,
library: { type: 'module' },
remoteType: 'module',
}),
);

//
// Dev server tweaks for `rspack serve`:
// - HMR is disabled: hot updates don't work across module-format federated
// boundaries yet and send the runtime into an aborted-chunk reload loop.
// (The original rsbuild integration disabled `hmr` for the same reason.)
// - CORS is enabled: loading module remotes across origins uses `import()`,
// which is subject to CORS, so a host can pull a remote's entry.
//
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(rspackConfig as any).devServer = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(rspackConfig as any).devServer,
hot: false,
liveReload: false,
headers: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...((rspackConfig as any).devServer?.headers ?? {}),
'Access-Control-Allow-Origin': '*',
},
};

return config;
return rspackConfig;
}

function isServerConfig(config: Configuration): boolean {
const target = config.target;
return (
target === 'node' ||
target === 'async-node' ||
(Array.isArray(target) && target.includes('node'))
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { withFederation, shareAll } from '@angular-architects/module-federation/

export default withFederation({
options: {
<% if (type === 'remote') { %>
// rspack's Module Federation container requires a unique name.
name: '<%=project%>',

<% if (type === 'remote') { %>
exposes: {
'./Component': './<%=appComponent%>',
},
<% } else if (type === 'host') { %>
remotes: {<% for (key in remoteMap) { %>
"<%=key%>": "<%=remoteMap[key]%>",<% } %>
"<%=key%>": "<%=remoteMap[key]%>",<% } %>
},
<% } %>
shared: {
Expand All @@ -19,7 +19,7 @@ export default withFederation({

},
skip: [
// Add the names of packages, entrypoints
// Add the names of packages, entrypoints
// and libs you don't want to share here
// Strings are compared with ===

Expand Down
Loading
Loading