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
10 changes: 10 additions & 0 deletions .changeset/mean-bulldogs-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@module-federation/rsbuild-plugin': patch
'@module-federation/enhanced': patch
'@module-federation/manifest': patch
'@module-federation/modern-js': patch
'@module-federation/rspack': patch
'@module-federation/sdk': patch
---

refactor(manifest): collect assets from build hook
7 changes: 6 additions & 1 deletion apps/website-new/docs/en/configure/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
- Type: `boolean | PluginManifestOptions`
- Default value: `undefined`

Used to control whether to generate a manifest and the corresponding generation configuration.
Controls whether the plugin generates manifest artifacts and how they are produced. When enabled, the plugin emits `mf-manifest.json` and `mf-stats.json` (names can be customized via `fileName`) on every build so that other tools can consume them directly through `processAssets` or from the final build output.

- `mf-stats.json`: captures the full build statistics, including the assets for exposes/shared/remotes, `metaData` (plugin version, build info, `remoteEntry`, etc.), and any additional asset analysis. Ideal for debugging or merging stats across environments.
- `mf-manifest.json`: a runtime-oriented manifest distilled from the stats. It keeps the stable structure that Module Federation consumers read when loading remote modules. The exposes/shared/remotes entries describe what is actually available to consumers.

The `PluginManifestOptions` types are as follows:

Expand All @@ -29,6 +32,8 @@ manifest filePath

manifest fileName

If `fileName` is provided, the companion stats file automatically receives a `-stats` suffix (for example, `fileName: 'mf.json'` produces both `mf.json` and `mf-stats.json`). Generated files are written under `filePath` when that option is set.

## disableAssetsAnalyze

:::warning
Expand Down
11 changes: 11 additions & 0 deletions apps/website-new/docs/en/guide/troubleshooting/other.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,14 @@ export default function MFLinkPlugin(): ModuleFederationRuntimePlugin {
};
}
```


## Multiple assets emit different content to the same filename mf-manifest.json

### Reason

In Rspack `1.6.0-beta.0`, we ported the manifest implementation to Rust. Upgrading Rspack without upgrading the MF-related packages will cause this error.

### Solution

Upgrade the `@module-federation` scoped npm package to version 0.21.0 or later.
7 changes: 6 additions & 1 deletion apps/website-new/docs/zh/configure/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
- 类型:`boolean | PluginManifestOptions`
- 默认值:`undefined`

用于控制是否生成 manifest ,以及对应的生成配置。
用于控制是否生成 manifest ,以及对应的生成配置。启用后插件会在每次构建中同时产出 `mf-manifest.json` 与 `mf-stats.json`(名称可通过 `fileName` 自定义),并写入到构建产物中,供其他工具在 `processAssets` 钩子或构建结果中直接读取。

- `mf-stats.json`:包含完整的构建统计信息,如 exposes/shared/remotes 的资源列表、`metaData`(插件版本、构建信息、`remoteEntry` 等)以及额外的资产分析结果,适合用于后续合并或诊断。
- `mf-manifest.json`:在 stats 基础上提炼出的运行时清单,结构稳定,供 Module Federation 消费端在加载远程模块时读取。文件中的 exposes/shared/remotes 对应线上对外暴露的能力。

`PluginManifestOptions` 类型如下:

Expand All @@ -29,6 +32,8 @@ manifest 存放路径

manifest 文件名称

如果设置了 `fileName`,对应的 stats 文件名会自动附加 `-stats` 后缀(例如 `fileName: 'mf.json'` 时会同时生成 `mf.json` 与 `mf-stats.json`)。所有文件都会写入 `filePath`(若配置)指定的子目录。

## disableAssetsAnalyze

:::warning
Expand Down
10 changes: 10 additions & 0 deletions apps/website-new/docs/zh/guide/troubleshooting/other.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,13 @@ export default function MFLinkPlugin(): ModuleFederationRuntimePlugin {
};
}
```

## Multiple assets emit different content to the same filename mf-manifest.json

### 原因

在 Rspack `1.6.0-beta.0` 中,我们将 manifest 实现移植到了 Rust 侧,如果升级了 Rspack 而没有升级 MF 相关的 package 则会造成此错误。

### 解决方案

升级 `@module-federation` scope 下的 npm 包至 `0.21.0` 及以上版本。
4 changes: 0 additions & 4 deletions packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,6 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
this._statsPlugin.apply(compiler);
}
}

get statsResourceInfo() {
return this._statsPlugin?.resourceInfo;
}
}

export default ModuleFederationPlugin;
4 changes: 0 additions & 4 deletions packages/enhanced/src/wrapper/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,4 @@ export default class ModuleFederationPlugin implements WebpackPluginInstance {
}).apply(compiler);
}
}

get statsResourceInfo(): Partial<ResourceInfo> | undefined {
return this._mfPlugin?.statsResourceInfo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const fs = __non_webpack_require__('fs');
const path = __non_webpack_require__('path');

const statsPath = path.join(__dirname, 'mf-stats.json');
const manifestPath = path.join(__dirname, 'mf-manifest.json');

const stats = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));

it('should still emit the remote entry', () => {
const remoteEntryFile = stats.metaData.remoteEntry.name;
const remoteEntryPath = path.join(__dirname, remoteEntryFile);
expect(fs.existsSync(remoteEntryPath)).toBe(true);
});

it('should omit asset details from stats when disableAssetsAnalyze is true', () => {
expect(stats.shared).toHaveLength(1);
expect(stats.shared[0].assets.js.sync).toEqual([]);
expect(stats.shared[0].assets.js.async).toEqual([]);
expect(stats.exposes).toHaveLength(1);
expect(stats.exposes[0].assets.js.sync).toEqual([]);
expect(stats.exposes[0].assets.js.async).toEqual([]);
});

it('should omit asset details from manifest when disableAssetsAnalyze is true', () => {
expect(manifest.shared).toHaveLength(1);
expect(manifest.shared[0].assets.js.sync).toEqual([]);
expect(manifest.shared[0].assets.js.async).toEqual([]);
expect(manifest.exposes).toHaveLength(1);
expect(manifest.exposes[0].assets.js.sync).toEqual([]);
expect(manifest.exposes[0].assets.js.async).toEqual([]);
});

it('should mark remote usage locations as UNKNOWN', () => {
expect(stats.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
usedIn: ['UNKNOWN'],
}),
]),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const lazy = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import react from 'react';
import remote from 'remote';

global.react = react;
global.remote = remote;

import('./lazy-module').then((r) => {
console.log('lazy module: ', r);
});

export const ok = true;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"react": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');

module.exports = {
optimization: {
chunkIds: 'named',
moduleIds: 'named',
},
output: {
publicPath: '/',
chunkFilename: '[id].js',
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
filename: 'container.[chunkhash:8].js',
library: { type: 'commonjs-module' },
exposes: {
'expose-a': './module.js',
},
remoteType: 'script',
remotes: {
remote: 'remote@http://localhost:8000/remoteEntry.js',
},
shared: {
react: {},
},
manifest: {
disableAssetsAnalyze: true,
},
}),
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const fs = __non_webpack_require__('fs');
const path = __non_webpack_require__('path');

const customPath = 'custom-path';
const customManifestPath = path.join(
__dirname,
customPath,
'custom-manifest.json',
);
const customStatsPath = path.join(
__dirname,
customPath,
'custom-manifest-stats.json',
);
const defaultManifestPath = path.join(__dirname, 'mf-manifest.json');
const defaultStatsPath = path.join(__dirname, 'mf-stats.json');

const stats = JSON.parse(fs.readFileSync(customStatsPath, 'utf-8'));
const manifest = JSON.parse(fs.readFileSync(customManifestPath, 'utf-8'));

it('should emit manifest with the configured fileName', () => {
expect(fs.existsSync(customManifestPath)).toBe(true);
expect(fs.existsSync(customStatsPath)).toBe(true);
});

it('should not emit default manifest file names when fileName is set', () => {
expect(fs.existsSync(defaultManifestPath)).toBe(false);
expect(fs.existsSync(defaultStatsPath)).toBe(false);
});

it('should still point to the emitted remote entry', () => {
const remoteEntryFile = stats.metaData.remoteEntry.name;
const remoteEntryPath = path.join(__dirname, remoteEntryFile);
expect(fs.existsSync(remoteEntryPath)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const lazy = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import react from 'react';
import remote from 'remote';

global.react = react;
global.remote = remote;

import('./lazy-module').then((r) => {
console.log('lazy module: ', r);
});

export const ok = true;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"react": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');

module.exports = {
optimization: {
chunkIds: 'named',
moduleIds: 'named',
},
output: {
publicPath: '/',
chunkFilename: '[id].js',
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
filename: 'container.[chunkhash:8].js',
library: { type: 'commonjs-module' },
exposes: {
'expose-a': './module.js',
},
remoteType: 'script',
remotes: {
remote: 'remote@http://localhost:8000/remoteEntry.js',
},
shared: {
react: {},
},
manifest: {
fileName: 'custom-manifest.json',
filePath: 'custom-path',
},
}),
],
};
93 changes: 93 additions & 0 deletions packages/enhanced/test/configCases/container/manifest/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const fs = __non_webpack_require__('fs');
const path = __non_webpack_require__('path');

const statsPath = path.join(__dirname, 'mf-stats.json');
const manifestPath = path.join(__dirname, 'mf-manifest.json');
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));

it('should emit remote entry with hash', () => {
const remoteEntryFile = stats.metaData.remoteEntry.name;
const remoteEntryPath = path.join(__dirname, remoteEntryFile);
expect(fs.existsSync(remoteEntryPath)).toBe(true);
});

// shared
it('should report shared assets in sync only', () => {
expect(stats.shared).toHaveLength(1);
expect(stats.shared[0].assets.js.sync.sort()).toEqual([
'node_modules_react_js.js',
]);
expect(stats.shared[0].assets.js.async).toEqual([]);
});

it('should materialize in manifest', () => {
expect(manifest.shared).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'react',
assets: expect.objectContaining({
js: expect.objectContaining({
sync: expect.arrayContaining(['node_modules_react_js.js']),
async: [],
}),
}),
}),
]),
);
});

//exposes
it('should expose sync assets only', () => {
expect(stats.exposes).toHaveLength(1);
expect(stats.exposes[0].assets.js.sync).toEqual([
'__federation_expose_expose_a.js',
]);
expect(stats.exposes[0].assets.js.async).toEqual(['lazy-module_js.js']);
});

it('should reflect expose assets in manifest', () => {
expect(manifest.exposes).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'expose-a',
path: './expose-a',
assets: expect.objectContaining({
js: expect.objectContaining({
sync: ['__federation_expose_expose_a.js'],
async: ['lazy-module_js.js'],
}),
}),
}),
]),
);
});

// remotes

it('should record remote usage', () => {
expect(stats.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
alias: '@remote/alias',
consumingFederationContainerName: 'container',
federationContainerName: 'remote',
moduleName: '.',
usedIn: expect.arrayContaining(['module.js']),
entry: 'http://localhost:8000/remoteEntry.js',
}),
]),
);
});

it('should persist remote metadata in manifest', () => {
expect(manifest.remotes).toEqual(
expect.arrayContaining([
expect.objectContaining({
alias: '@remote/alias',
federationContainerName: 'remote',
moduleName: '.',
}),
]),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const lazy = true;
Loading