Skip to content

Commit 374f5c2

Browse files
authored
refactor(metro): align metro plugin configuration with MF SDK (module-federation#4446)
1 parent a0faa70 commit 374f5c2

27 files changed

Lines changed: 784 additions & 100 deletions

.changeset/odd-snails-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/metro': patch
3+
---
4+
5+
refactor and harden Metro module federation config handling by deduplicating normalized runtime plugins, tightening option validation, and improving warnings for unsupported/deprecated options, including deprecating `plugins` in favor of `runtimePlugins`.

apps/metro-example-host/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2422,7 +2422,7 @@ SPEC CHECKSUMS:
24222422
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
24232423
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
24242424
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
2425-
ReactCodegen: d308d08c58717331dcf82d0129efa8b73e28a64c
2425+
ReactCodegen: 2539080349c02b1edbf525d0a392df99f984f34b
24262426
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
24272427
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
24282428
Yoga: 395b5d614cd7cbbfd76b05d01bd67230a6ad004e

apps/metro-example-host/metro.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ module.exports = withModuleFederation(
5252
},
5353
},
5454
shareStrategy: 'loaded-first',
55-
plugins: [path.resolve(__dirname, './runtime-plugin.ts')],
55+
runtimePlugins: [path.resolve(__dirname, './runtime-plugin.ts')],
5656
},
5757
{
5858
flags: {

apps/website-new/docs/en/guide/build-plugins/plugins-metro.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ export interface ModuleFederationConfig {
238238
exposes?: Record<string, string>;
239239
shared?: Shared;
240240
shareStrategy?: 'loaded-first' | 'version-first';
241+
runtimePlugins?: string[];
242+
/**
243+
* @deprecated Use runtimePlugins instead.
244+
* Scheduled for removal in the next major version.
245+
*/
241246
plugins?: string[];
242247
}
243248
```
@@ -254,6 +259,14 @@ export interface SharedConfig {
254259
}
255260
```
256261

262+
#### Unsupported Option Warnings
263+
264+
Metro currently supports only a subset of the shared Module Federation config options.
265+
When unsupported options are provided, Metro emits warnings during validation that those options are **not supported and will have no effect**.
266+
267+
Use `runtimePlugins` for runtime plugin paths.
268+
`plugins` is deprecated and will be removed in the next major version.
269+
257270
## Examples and Best Practices
258271

259272
The configuration follows the standard [Module Federation configuration format](https://module-federation.io/configure/). For comprehensive information about Module Federation concepts, configuration options, and usage patterns, please refer to the official [Module Federation documentation](https://module-federation.io/).

apps/website-new/docs/zh/guide/build-plugins/plugins-metro.mdx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ export interface ModuleFederationConfig {
240240
exposes?: Record<string, string>;
241241
shared?: Shared;
242242
shareStrategy?: 'loaded-first' | 'version-first';
243+
runtimePlugins?: string[];
244+
/**
245+
* @deprecated 请改用 runtimePlugins。
246+
* 计划在下一个 major 版本移除。
247+
*/
243248
plugins?: string[];
244249
}
245250
```
@@ -256,9 +261,17 @@ export interface SharedConfig {
256261
}
257262
```
258263

264+
#### 不支持选项告警
265+
266+
Metro 当前只支持共享 Module Federation 配置中的一部分选项。
267+
当传入不支持的选项时,Metro 会在校验阶段输出告警,明确这些选项**不受支持且不会生效**
268+
269+
运行时插件路径请使用 `runtimePlugins`
270+
`plugins` 已废弃,并将在下一个 major 版本移除。
271+
259272
## 示例与最佳实践
260273

261274
配置遵循标准的 [模块联邦配置格式](https://module-federation.io/configure/)
262275
关于模块联邦的概念、配置选项和使用模式的完整信息,请参考官方 [模块联邦文档](https://module-federation.io/)
263276

264-
要查看可运行的示例和详细实现指南,请访问 [Module Federation Metro 仓库](https://github.com/module-federation/metro),其中包含多个示例应用,展示了不同的使用模式和集成方式。
277+
要查看可运行的示例和详细实现指南,请访问 [Module Federation Metro 仓库](https://github.com/module-federation/metro),其中包含多个示例应用,展示了不同的使用模式和集成方式。
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import path from 'node:path';
2+
import { vol } from 'memfs';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
5+
vi.mock('node:fs', () => {
6+
const memfs = require('memfs').fs;
7+
return { ...memfs, default: memfs };
8+
});
9+
10+
import { normalizeOptions } from '../../src/plugin/normalize-options';
11+
12+
let projectCount = 0;
13+
14+
function createProjectRoot() {
15+
projectCount += 1;
16+
const projectRoot = `/virtual/metro-core-${projectCount}`;
17+
vol.fromJSON({
18+
[path.join(projectRoot, 'package.json')]: JSON.stringify({
19+
dependencies: {
20+
react: '19.1.0',
21+
'react-native': '0.80.0',
22+
},
23+
}),
24+
});
25+
return projectRoot;
26+
}
27+
28+
function getShared() {
29+
return {
30+
react: {
31+
singleton: true,
32+
eager: false,
33+
version: '19.1.0',
34+
requiredVersion: '19.1.0',
35+
},
36+
'react-native': {
37+
singleton: true,
38+
eager: false,
39+
version: '0.80.0',
40+
requiredVersion: '0.80.0',
41+
},
42+
};
43+
}
44+
45+
describe('normalizeOptions', () => {
46+
afterEach(() => {
47+
vol.reset();
48+
vi.restoreAllMocks();
49+
});
50+
51+
it('supports runtimePlugins as the primary config field', () => {
52+
const projectRoot = createProjectRoot();
53+
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
54+
vol.mkdirSync(tmpDirPath, { recursive: true });
55+
56+
const runtimePluginPath = path.join(projectRoot, 'runtime-plugin.js');
57+
vol.writeFileSync(runtimePluginPath, 'module.exports = () => ({})');
58+
59+
const normalized = normalizeOptions(
60+
{
61+
name: 'MetroHost',
62+
shared: getShared(),
63+
runtimePlugins: [runtimePluginPath],
64+
} as any,
65+
{ projectRoot, tmpDirPath },
66+
);
67+
68+
const metroCorePluginPath = require.resolve(
69+
'../../src/modules/metroCorePlugin.ts',
70+
);
71+
expect(normalized.plugins).toEqual([
72+
path.relative(tmpDirPath, metroCorePluginPath),
73+
path.relative(tmpDirPath, runtimePluginPath),
74+
]);
75+
});
76+
77+
it('deduplicates runtime plugins while preserving order', () => {
78+
const projectRoot = createProjectRoot();
79+
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
80+
vol.mkdirSync(tmpDirPath, { recursive: true });
81+
82+
const runtimePluginPath = path.join(projectRoot, 'runtime-plugin.js');
83+
const runtimePluginTwoPath = path.join(
84+
projectRoot,
85+
'runtime-plugin-two.js',
86+
);
87+
vol.writeFileSync(runtimePluginPath, 'module.exports = () => ({})');
88+
vol.writeFileSync(runtimePluginTwoPath, 'module.exports = () => ({})');
89+
90+
const normalized = normalizeOptions(
91+
{
92+
name: 'MetroHost',
93+
shared: getShared(),
94+
runtimePlugins: [
95+
runtimePluginPath,
96+
runtimePluginPath,
97+
runtimePluginTwoPath,
98+
],
99+
} as any,
100+
{ projectRoot, tmpDirPath },
101+
);
102+
103+
const metroCorePluginPath = require.resolve(
104+
'../../src/modules/metroCorePlugin.ts',
105+
);
106+
expect(normalized.plugins).toEqual([
107+
path.relative(tmpDirPath, metroCorePluginPath),
108+
path.relative(tmpDirPath, runtimePluginPath),
109+
path.relative(tmpDirPath, runtimePluginTwoPath),
110+
]);
111+
});
112+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import { validateOptions } from '../../src/plugin/validate-options';
3+
4+
function getValidConfig() {
5+
return {
6+
name: 'MetroHost',
7+
filename: 'mf-manifest.bundle',
8+
remotes: {},
9+
shared: {
10+
react: {
11+
singleton: true,
12+
eager: true,
13+
version: '19.1.0',
14+
requiredVersion: '19.1.0',
15+
},
16+
'react-native': {
17+
singleton: true,
18+
eager: true,
19+
version: '0.80.0',
20+
requiredVersion: '0.80.0',
21+
},
22+
},
23+
};
24+
}
25+
26+
describe('validateOptions', () => {
27+
afterEach(() => {
28+
vi.restoreAllMocks();
29+
});
30+
31+
it('warns when unsupported options are configured', () => {
32+
const warnSpy = vi
33+
.spyOn(console, 'warn')
34+
.mockImplementation(() => undefined);
35+
36+
validateOptions({
37+
...getValidConfig(),
38+
manifest: true,
39+
} as any);
40+
41+
expect(warnSpy).toHaveBeenCalled();
42+
expect(warnSpy.mock.calls.join('\n')).toContain('manifest');
43+
expect(warnSpy.mock.calls.join('\n')).toContain('will have no effect');
44+
});
45+
46+
it('warns that runtime plugin params are not supported', () => {
47+
const warnSpy = vi
48+
.spyOn(console, 'warn')
49+
.mockImplementation(() => undefined);
50+
51+
validateOptions({
52+
...getValidConfig(),
53+
runtimePlugins: [['/tmp/runtime-plugin.js', { answer: 42 }]],
54+
} as any);
55+
56+
expect(warnSpy).toHaveBeenCalled();
57+
expect(warnSpy.mock.calls.join('\n')).toContain('runtimePlugins[0][1]');
58+
expect(warnSpy.mock.calls.join('\n')).toContain('will have no effect');
59+
});
60+
61+
it('does not warn for runtime plugin tuple without params', () => {
62+
const warnSpy = vi
63+
.spyOn(console, 'warn')
64+
.mockImplementation(() => undefined);
65+
66+
validateOptions({
67+
...getValidConfig(),
68+
runtimePlugins: [['/tmp/runtime-plugin.js']],
69+
} as any);
70+
71+
expect(warnSpy).not.toHaveBeenCalled();
72+
});
73+
74+
it('warns when deprecated plugins is used', () => {
75+
const warnSpy = vi
76+
.spyOn(console, 'warn')
77+
.mockImplementation(() => undefined);
78+
79+
validateOptions({
80+
...getValidConfig(),
81+
plugins: ['/tmp/runtime-plugin.js'],
82+
} as any);
83+
84+
expect(warnSpy).toHaveBeenCalled();
85+
expect(warnSpy.mock.calls.join('\n')).toContain('deprecated');
86+
expect(warnSpy.mock.calls.join('\n')).toContain('runtimePlugins');
87+
});
88+
89+
it('throws for unsupported advanced remotes format', () => {
90+
expect(() =>
91+
validateOptions({
92+
...getValidConfig(),
93+
remotes: {
94+
mini: {
95+
external: 'mini@http://localhost:8081/mf-manifest.json',
96+
},
97+
},
98+
} as any),
99+
).toThrow('remotes');
100+
});
101+
102+
it('throws for unsupported shared shorthand format', () => {
103+
expect(() =>
104+
validateOptions({
105+
...getValidConfig(),
106+
shared: {
107+
react: 'react',
108+
'react-native': {
109+
singleton: true,
110+
eager: true,
111+
version: '0.80.0',
112+
requiredVersion: '0.80.0',
113+
},
114+
},
115+
} as any),
116+
).toThrow('shared');
117+
});
118+
});

0 commit comments

Comments
 (0)