Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/odd-snails-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/metro': patch
---

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`.
2 changes: 1 addition & 1 deletion apps/metro-example-host/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2422,7 +2422,7 @@ SPEC CHECKSUMS:
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
ReactCodegen: d308d08c58717331dcf82d0129efa8b73e28a64c
ReactCodegen: 2539080349c02b1edbf525d0a392df99f984f34b
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 395b5d614cd7cbbfd76b05d01bd67230a6ad004e
Expand Down
2 changes: 1 addition & 1 deletion apps/metro-example-host/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module.exports = withModuleFederation(
},
},
shareStrategy: 'loaded-first',
plugins: [path.resolve(__dirname, './runtime-plugin.ts')],
runtimePlugins: [path.resolve(__dirname, './runtime-plugin.ts')],
},
{
flags: {
Expand Down
13 changes: 13 additions & 0 deletions apps/website-new/docs/en/guide/build-plugins/plugins-metro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ export interface ModuleFederationConfig {
exposes?: Record<string, string>;
shared?: Shared;
shareStrategy?: 'loaded-first' | 'version-first';
runtimePlugins?: string[];
/**
* @deprecated Use runtimePlugins instead.
* Scheduled for removal in the next major version.
*/
plugins?: string[];
}
```
Expand All @@ -254,6 +259,14 @@ export interface SharedConfig {
}
```

#### Unsupported Option Warnings

Metro currently supports only a subset of the shared Module Federation config options.
When unsupported options are provided, Metro emits warnings during validation that those options are **not supported and will have no effect**.

Use `runtimePlugins` for runtime plugin paths.
`plugins` is deprecated and will be removed in the next major version.

## Examples and Best Practices

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/).
Expand Down
15 changes: 14 additions & 1 deletion apps/website-new/docs/zh/guide/build-plugins/plugins-metro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ export interface ModuleFederationConfig {
exposes?: Record<string, string>;
shared?: Shared;
shareStrategy?: 'loaded-first' | 'version-first';
runtimePlugins?: string[];
/**
* @deprecated 请改用 runtimePlugins。
* 计划在下一个 major 版本移除。
*/
plugins?: string[];
}
```
Expand All @@ -256,9 +261,17 @@ export interface SharedConfig {
}
```

#### 不支持选项告警

Metro 当前只支持共享 Module Federation 配置中的一部分选项。
当传入不支持的选项时,Metro 会在校验阶段输出告警,明确这些选项**不受支持且不会生效**。

运行时插件路径请使用 `runtimePlugins`。
`plugins` 已废弃,并将在下一个 major 版本移除。

## 示例与最佳实践

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

要查看可运行的示例和详细实现指南,请访问 [Module Federation Metro 仓库](https://github.com/module-federation/metro),其中包含多个示例应用,展示了不同的使用模式和集成方式。
要查看可运行的示例和详细实现指南,请访问 [Module Federation Metro 仓库](https://github.com/module-federation/metro),其中包含多个示例应用,展示了不同的使用模式和集成方式。
112 changes: 112 additions & 0 deletions packages/metro-core/__tests__/plugin/normalize-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import path from 'node:path';
import { vol } from 'memfs';
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('node:fs', () => {
const memfs = require('memfs').fs;
return { ...memfs, default: memfs };
});

import { normalizeOptions } from '../../src/plugin/normalize-options';

let projectCount = 0;

function createProjectRoot() {
projectCount += 1;
const projectRoot = `/virtual/metro-core-${projectCount}`;
vol.fromJSON({
[path.join(projectRoot, 'package.json')]: JSON.stringify({
dependencies: {
react: '19.1.0',
'react-native': '0.80.0',
},
}),
});
return projectRoot;
}

function getShared() {
return {
react: {
singleton: true,
eager: false,
version: '19.1.0',
requiredVersion: '19.1.0',
},
'react-native': {
singleton: true,
eager: false,
version: '0.80.0',
requiredVersion: '0.80.0',
},
};
}

describe('normalizeOptions', () => {
afterEach(() => {
vol.reset();
vi.restoreAllMocks();
});

it('supports runtimePlugins as the primary config field', () => {
const projectRoot = createProjectRoot();
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
vol.mkdirSync(tmpDirPath, { recursive: true });

const runtimePluginPath = path.join(projectRoot, 'runtime-plugin.js');
vol.writeFileSync(runtimePluginPath, 'module.exports = () => ({})');

const normalized = normalizeOptions(
{
name: 'MetroHost',
shared: getShared(),
runtimePlugins: [runtimePluginPath],
} as any,
{ projectRoot, tmpDirPath },
);

const metroCorePluginPath = require.resolve(
'../../src/modules/metroCorePlugin.ts',
);
expect(normalized.plugins).toEqual([
path.relative(tmpDirPath, metroCorePluginPath),
path.relative(tmpDirPath, runtimePluginPath),
]);
});

it('deduplicates runtime plugins while preserving order', () => {
const projectRoot = createProjectRoot();
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
vol.mkdirSync(tmpDirPath, { recursive: true });

const runtimePluginPath = path.join(projectRoot, 'runtime-plugin.js');
const runtimePluginTwoPath = path.join(
projectRoot,
'runtime-plugin-two.js',
);
vol.writeFileSync(runtimePluginPath, 'module.exports = () => ({})');
vol.writeFileSync(runtimePluginTwoPath, 'module.exports = () => ({})');

const normalized = normalizeOptions(
{
name: 'MetroHost',
shared: getShared(),
runtimePlugins: [
runtimePluginPath,
runtimePluginPath,
runtimePluginTwoPath,
],
} as any,
{ projectRoot, tmpDirPath },
);

const metroCorePluginPath = require.resolve(
'../../src/modules/metroCorePlugin.ts',
);
expect(normalized.plugins).toEqual([
path.relative(tmpDirPath, metroCorePluginPath),
path.relative(tmpDirPath, runtimePluginPath),
path.relative(tmpDirPath, runtimePluginTwoPath),
]);
});
});
118 changes: 118 additions & 0 deletions packages/metro-core/__tests__/plugin/validate-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { validateOptions } from '../../src/plugin/validate-options';

function getValidConfig() {
return {
name: 'MetroHost',
filename: 'mf-manifest.bundle',
remotes: {},
shared: {
react: {
singleton: true,
eager: true,
version: '19.1.0',
requiredVersion: '19.1.0',
},
'react-native': {
singleton: true,
eager: true,
version: '0.80.0',
requiredVersion: '0.80.0',
},
},
};
}

describe('validateOptions', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('warns when unsupported options are configured', () => {
const warnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined);

validateOptions({
...getValidConfig(),
manifest: true,
} as any);

expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls.join('\n')).toContain('manifest');
expect(warnSpy.mock.calls.join('\n')).toContain('will have no effect');
});

it('warns that runtime plugin params are not supported', () => {
const warnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined);

validateOptions({
...getValidConfig(),
runtimePlugins: [['/tmp/runtime-plugin.js', { answer: 42 }]],
} as any);

expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls.join('\n')).toContain('runtimePlugins[0][1]');
expect(warnSpy.mock.calls.join('\n')).toContain('will have no effect');
});

it('does not warn for runtime plugin tuple without params', () => {
const warnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined);

validateOptions({
...getValidConfig(),
runtimePlugins: [['/tmp/runtime-plugin.js']],
} as any);

expect(warnSpy).not.toHaveBeenCalled();
});

it('warns when deprecated plugins is used', () => {
const warnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined);

validateOptions({
...getValidConfig(),
plugins: ['/tmp/runtime-plugin.js'],
} as any);

expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls.join('\n')).toContain('deprecated');
expect(warnSpy.mock.calls.join('\n')).toContain('runtimePlugins');
});

it('throws for unsupported advanced remotes format', () => {
expect(() =>
validateOptions({
...getValidConfig(),
remotes: {
mini: {
external: 'mini@http://localhost:8081/mf-manifest.json',
},
},
} as any),
).toThrow('remotes');
});

it('throws for unsupported shared shorthand format', () => {
expect(() =>
validateOptions({
...getValidConfig(),
shared: {
react: 'react',
'react-native': {
singleton: true,
eager: true,
version: '0.80.0',
requiredVersion: '0.80.0',
},
},
} as any),
).toThrow('shared');
});
});
Loading
Loading