@ice/mf-runtime 1.0.2-beta.3 → 1.0.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -1
- package/es2017/RemoteModule.js +73 -21
- package/es2017/__tests__/plugin-manager.test.d.ts +1 -0
- package/es2017/__tests__/plugin-manager.test.js +291 -0
- package/es2017/__tests__/setup.d.ts +1 -0
- package/es2017/__tests__/setup.js +28 -0
- package/es2017/index.d.ts +2 -0
- package/es2017/index.js +6 -4
- package/es2017/plugin-manager.d.ts +74 -0
- package/es2017/plugin-manager.js +128 -0
- package/es2017/runtime-plugin.js +29 -30
- package/es2017/types.d.ts +67 -0
- package/es2017/types.js +18 -1
- package/esm/RemoteModule.js +73 -30
- package/esm/__tests__/plugin-manager.test.d.ts +1 -0
- package/esm/__tests__/plugin-manager.test.js +343 -0
- package/esm/__tests__/setup.d.ts +1 -0
- package/esm/__tests__/setup.js +43 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +6 -4
- package/esm/plugin-manager.d.ts +74 -0
- package/esm/plugin-manager.js +201 -0
- package/esm/runtime-plugin.js +33 -40
- package/esm/types.d.ts +67 -0
- package/esm/types.js +20 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# @ice/mf-runtime
|
|
2
2
|
|
|
3
|
-
基于 Module Federation 的运行时工具,用于在 ice.js 应用中加载和管理远程模块。支持跨版本 React
|
|
3
|
+
基于 Module Federation 的运行时工具,用于在 ice.js 应用中加载和管理远程模块。支持跨版本 React 组件加载,并提供增强的插件机制。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
7
|
- 基于 [Module Federation 2.0](https://module-federation.io/index.html)
|
|
8
8
|
- 支持跨版本 React 组件加载
|
|
9
9
|
- 内置冲突检测和降级渲染
|
|
10
|
+
- 🔌 **增强插件机制**: 完全兼容标准 MF 运行时插件
|
|
11
|
+
- 🎯 **组件增强**: 支持 HOC、Props 注入等组件扩展机制
|
|
12
|
+
- 🛡️ **错误边界**: 内置错误处理和恢复机制
|
|
13
|
+
- 📊 **性能监控**: 支持组件加载和渲染性能追踪
|
|
14
|
+
- 🔄 **热插拔**: 运行时动态注册和移除插件
|
|
15
|
+
- 📝 **TypeScript**: 完整的类型定义支持
|
|
10
16
|
|
|
11
17
|
## 安装
|
|
12
18
|
|
|
@@ -14,6 +20,220 @@
|
|
|
14
20
|
$ npm i @ice/mf-runtime --save
|
|
15
21
|
```
|
|
16
22
|
|
|
23
|
+
## 插件机制
|
|
24
|
+
|
|
25
|
+
从 v1.0.1 开始,`@ice/mf-runtime` 支持强大的插件机制,允许在运行时动态扩展微前端功能。
|
|
26
|
+
|
|
27
|
+
### 插件类型
|
|
28
|
+
|
|
29
|
+
#### 1. 标准 MF 插件 (FederationRuntimePlugin)
|
|
30
|
+
标准 MF 插件使用原生的 `@module-federation/runtime` API 进行管理:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { registerPlugins } from '@module-federation/runtime';
|
|
34
|
+
import type { FederationRuntimePlugin } from '@ice/mf-runtime';
|
|
35
|
+
|
|
36
|
+
const mfPlugin: FederationRuntimePlugin = {
|
|
37
|
+
name: 'my-mf-plugin',
|
|
38
|
+
beforeRequest: (args) => {
|
|
39
|
+
console.log('模块请求前:', args);
|
|
40
|
+
return args;
|
|
41
|
+
},
|
|
42
|
+
onLoad: (args) => {
|
|
43
|
+
console.log('模块加载后:', args);
|
|
44
|
+
return args;
|
|
45
|
+
},
|
|
46
|
+
afterResolve: (args) => {
|
|
47
|
+
console.log('模块解析后:', args);
|
|
48
|
+
return args;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 使用原生 API 注册标准 MF 插件
|
|
53
|
+
registerPlugins([mfPlugin]);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### 2. 增强插件 (EnhancedRuntimePlugin)
|
|
57
|
+
增强插件使用新的 API 进行管理,提供组件级别的扩展能力:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { registerEnhancedPlugins } from '@ice/mf-runtime';
|
|
61
|
+
import type { EnhancedRuntimePlugin } from '@ice/mf-runtime';
|
|
62
|
+
|
|
63
|
+
const enhancedPlugin: EnhancedRuntimePlugin = {
|
|
64
|
+
name: 'my-enhanced-plugin',
|
|
65
|
+
|
|
66
|
+
// 组件包装器 - 支持 HOC 模式
|
|
67
|
+
wrapComponent: (WrappedComponent, context) => {
|
|
68
|
+
return function Enhanced(props) {
|
|
69
|
+
console.log(`渲染组件: ${context.remoteName}/${context.moduleName}`);
|
|
70
|
+
return <WrappedComponent {...props} />;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// 属性注入器
|
|
75
|
+
injectProps: (props, context) => {
|
|
76
|
+
return {
|
|
77
|
+
...props,
|
|
78
|
+
injectedProp: `来自 ${context.remoteName} 的数据`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 使用新的 API 注册增强插件
|
|
84
|
+
registerEnhancedPlugins([enhancedPlugin]);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 注册和使用插件
|
|
88
|
+
|
|
89
|
+
#### 基本用法
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { init, registerEnhancedPlugins, RemoteModule } from '@ice/mf-runtime';
|
|
93
|
+
import { registerPlugins } from '@module-federation/runtime';
|
|
94
|
+
|
|
95
|
+
// 注册标准 MF 插件
|
|
96
|
+
registerPlugins([mfPlugin]);
|
|
97
|
+
|
|
98
|
+
// 注册增强插件
|
|
99
|
+
registerEnhancedPlugins([enhancedPlugin]);
|
|
100
|
+
|
|
101
|
+
// 初始化运行时
|
|
102
|
+
init({
|
|
103
|
+
remotes: [
|
|
104
|
+
{
|
|
105
|
+
name: 'remote-app',
|
|
106
|
+
entry: 'http://localhost:3001/remoteEntry.js'
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 使用远程模块(插件会自动应用)
|
|
112
|
+
function App() {
|
|
113
|
+
return (
|
|
114
|
+
<RemoteModule
|
|
115
|
+
scope="remote-app"
|
|
116
|
+
module="Button"
|
|
117
|
+
componentProps={{ text: '点击我' }}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### 使用示例插件包
|
|
124
|
+
|
|
125
|
+
我们提供了一个示例插件包 `@ice/mf-plugin-example`,包含常用的插件:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install @ice/mf-plugin-example
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { registerPlugins } from '@module-federation/runtime';
|
|
133
|
+
import { registerEnhancedPlugins } from '@ice/mf-runtime';
|
|
134
|
+
import {
|
|
135
|
+
errorBoundaryPlugin,
|
|
136
|
+
commonPropsPlugin,
|
|
137
|
+
developmentMFLoggingPlugin,
|
|
138
|
+
developmentEnhancedLoggingPlugin
|
|
139
|
+
} from '@ice/mf-plugin-example';
|
|
140
|
+
|
|
141
|
+
// 注册标准 MF 插件
|
|
142
|
+
registerPlugins([
|
|
143
|
+
developmentMFLoggingPlugin // MF 日志插件
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
// 注册增强插件
|
|
147
|
+
registerEnhancedPlugins([
|
|
148
|
+
developmentEnhancedLoggingPlugin, // 增强日志插件
|
|
149
|
+
errorBoundaryPlugin, // 错误边界插件
|
|
150
|
+
commonPropsPlugin // 通用属性注入插件
|
|
151
|
+
]);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 插件开发指南
|
|
155
|
+
|
|
156
|
+
#### 错误边界插件示例
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
160
|
+
|
|
161
|
+
const errorBoundaryPlugin: EnhancedRuntimePlugin = {
|
|
162
|
+
name: 'error-boundary',
|
|
163
|
+
wrapComponent: (WrappedComponent, context) => {
|
|
164
|
+
return function ErrorBoundaryWrapper(props) {
|
|
165
|
+
return (
|
|
166
|
+
<ErrorBoundary
|
|
167
|
+
fallback={<div>组件 {context.remoteName} 加载失败</div>}
|
|
168
|
+
onError={(error) => {
|
|
169
|
+
console.error(`组件错误:`, error);
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<WrappedComponent {...props} />
|
|
173
|
+
</ErrorBoundary>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### 属性注入插件示例
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const themePlugin: EnhancedRuntimePlugin = {
|
|
184
|
+
name: 'theme-injector',
|
|
185
|
+
injectProps: (props, context) => {
|
|
186
|
+
return {
|
|
187
|
+
...props,
|
|
188
|
+
theme: {
|
|
189
|
+
mode: 'light',
|
|
190
|
+
primaryColor: '#1890ff'
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 插件 API
|
|
198
|
+
|
|
199
|
+
#### EnhancedPluginManager
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { getEnhancedPluginManager } from '@ice/mf-runtime';
|
|
203
|
+
|
|
204
|
+
const manager = getEnhancedPluginManager();
|
|
205
|
+
|
|
206
|
+
// 注册增强插件
|
|
207
|
+
manager.register(enhancedPlugin);
|
|
208
|
+
|
|
209
|
+
// 获取插件信息
|
|
210
|
+
const info = manager.getPluginInfo();
|
|
211
|
+
// 返回: [{ name: 'plugin-name', hasWrapper: true, hasInjector: false }]
|
|
212
|
+
|
|
213
|
+
// 移除插件
|
|
214
|
+
manager.removePlugin('plugin-name');
|
|
215
|
+
|
|
216
|
+
// 清空所有插件
|
|
217
|
+
manager.clear();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### React Hook
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { useEnhancedPluginManager } from '@ice/mf-runtime';
|
|
224
|
+
|
|
225
|
+
function MyComponent() {
|
|
226
|
+
const enhancedPluginManager = useEnhancedPluginManager();
|
|
227
|
+
|
|
228
|
+
// 动态操作插件
|
|
229
|
+
const handleAddPlugin = () => {
|
|
230
|
+
enhancedPluginManager.register(newEnhancedPlugin);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return <div>...</div>;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
17
237
|
## 使用
|
|
18
238
|
|
|
19
239
|
### 1. 初始化配置
|
package/es2017/RemoteModule.js
CHANGED
|
@@ -2,10 +2,13 @@ import { _ as _object_spread } from "@swc/helpers/_/_object_spread";
|
|
|
2
2
|
import { loadRemote } from '@module-federation/runtime';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { useMemo, useState, useLayoutEffect, forwardRef } from 'react';
|
|
5
|
+
import * as ReactDOM from 'react-dom';
|
|
5
6
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
6
7
|
import { FallBack } from './FallBack';
|
|
7
8
|
import { setFederatedModulePublicPath } from './set-public-path';
|
|
8
9
|
import { getMicroMod } from './mf-global-store';
|
|
10
|
+
import { useEnhancedPluginManager } from './plugin-manager';
|
|
11
|
+
import { isRuntimePluginWrapper } from './types';
|
|
9
12
|
const useMountNode = (mountNodeConfig)=>{
|
|
10
13
|
const [resolvedNode, setResolvedNode] = useState(undefined);
|
|
11
14
|
// 解析各种类型的 mountNode
|
|
@@ -35,6 +38,7 @@ const RemoteModuleInner = ({ module, scope, runtime, publicPath, LoadingComponen
|
|
|
35
38
|
var _microMod, _runtime, _runtime1;
|
|
36
39
|
const microMod = getMicroMod(scope);
|
|
37
40
|
const resolvedMountNode = useMountNode(mountNode);
|
|
41
|
+
const enhancedPluginManager = useEnhancedPluginManager();
|
|
38
42
|
if ((_microMod = microMod) === null || _microMod === void 0 ? void 0 : _microMod.publicPath) {
|
|
39
43
|
setFederatedModulePublicPath(microMod.moduleFederatedName, microMod.publicPath);
|
|
40
44
|
}
|
|
@@ -45,35 +49,76 @@ const RemoteModuleInner = ({ module, scope, runtime, publicPath, LoadingComponen
|
|
|
45
49
|
if (!module || !scope) return null;
|
|
46
50
|
return /*#__PURE__*/ React.lazy(async ()=>{
|
|
47
51
|
var _typedRemoteModule;
|
|
52
|
+
// 使用标准 MF 加载逻辑,MF 插件自动执行
|
|
48
53
|
const remoteModule = await loadRemote(`${scope}/${module}`);
|
|
49
|
-
//
|
|
50
|
-
if (
|
|
51
|
-
//
|
|
52
|
-
const
|
|
54
|
+
// 使用清晰的对象结构检查是否为 Runtime Plugin 包装器
|
|
55
|
+
if (isRuntimePluginWrapper(remoteModule)) {
|
|
56
|
+
// 提取原始组件、运行时信息和 React 实例
|
|
57
|
+
const { originalComponent, runtimeInfo, reactInstances } = remoteModule();
|
|
58
|
+
const remoteReact = reactInstances.react;
|
|
59
|
+
const remoteReactDOM = reactInstances.reactDOM;
|
|
60
|
+
// 🔑 正确的 HOC 层级顺序:
|
|
61
|
+
// 1. 先应用增强插件(与其他情况保持一致)
|
|
62
|
+
const PluginWrappedComponent = enhancedPluginManager.wrapComponent(originalComponent, {
|
|
63
|
+
remoteName: scope,
|
|
64
|
+
moduleName: module,
|
|
65
|
+
props: componentProps || {},
|
|
66
|
+
React: remoteReact,
|
|
67
|
+
ReactDOM: remoteReactDOM
|
|
68
|
+
});
|
|
69
|
+
// 2. 再应用 FallBack(处理不同 React 版本和 mountNode)
|
|
70
|
+
const FinalComponent = FallBack({
|
|
71
|
+
Original: PluginWrappedComponent,
|
|
72
|
+
remoteVersion: runtimeInfo.remoteVersion ? ()=>runtimeInfo.remoteVersion : undefined,
|
|
73
|
+
hostVersion: runtimeInfo.hostVersion ? ()=>runtimeInfo.hostVersion : undefined,
|
|
74
|
+
remoteReactDOM: runtimeInfo.remoteReactDOM,
|
|
75
|
+
remoteReact: runtimeInfo.remoteReact,
|
|
76
|
+
mountNode: resolvedMountNode,
|
|
77
|
+
containerClassName: fallbackContainerClassName
|
|
78
|
+
});
|
|
53
79
|
return {
|
|
54
|
-
default:
|
|
80
|
+
default: FinalComponent
|
|
55
81
|
};
|
|
56
82
|
}
|
|
57
83
|
const typedRemoteModule = remoteModule;
|
|
84
|
+
let BaseComponent;
|
|
58
85
|
if (!((_typedRemoteModule = typedRemoteModule) === null || _typedRemoteModule === void 0 ? void 0 : _typedRemoteModule.default)) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
BaseComponent = remoteModule;
|
|
87
|
+
} else {
|
|
88
|
+
BaseComponent = typedRemoteModule.default;
|
|
62
89
|
}
|
|
90
|
+
let FinalComponent;
|
|
91
|
+
// 如果需要 runtime 处理(不同 React 版本或 mountNode)
|
|
63
92
|
if (runtime) {
|
|
64
93
|
const { react, reactDOM } = runtime;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
// 先应用增强插件的组件包装器,然后传给 FallBack
|
|
95
|
+
const PluginWrappedComponent = enhancedPluginManager.wrapComponent(BaseComponent, {
|
|
96
|
+
remoteName: scope,
|
|
97
|
+
moduleName: module,
|
|
98
|
+
props: componentProps || {},
|
|
99
|
+
React: react,
|
|
100
|
+
ReactDOM: reactDOM
|
|
101
|
+
});
|
|
102
|
+
FinalComponent = FallBack({
|
|
103
|
+
Original: PluginWrappedComponent,
|
|
104
|
+
remoteReact: ()=>react,
|
|
105
|
+
remoteReactDOM: ()=>reactDOM,
|
|
106
|
+
mountNode: resolvedMountNode,
|
|
107
|
+
containerClassName: fallbackContainerClassName
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
// 没有 runtime 需求时,直接应用插件包装器
|
|
111
|
+
FinalComponent = enhancedPluginManager.wrapComponent(BaseComponent, {
|
|
112
|
+
remoteName: scope,
|
|
113
|
+
moduleName: module,
|
|
114
|
+
props: componentProps || {},
|
|
115
|
+
React: React,
|
|
116
|
+
ReactDOM: ReactDOM
|
|
117
|
+
});
|
|
74
118
|
}
|
|
119
|
+
console.log('FinalComponent', FinalComponent);
|
|
75
120
|
return {
|
|
76
|
-
default:
|
|
121
|
+
default: FinalComponent
|
|
77
122
|
};
|
|
78
123
|
});
|
|
79
124
|
}, [
|
|
@@ -82,11 +127,18 @@ const RemoteModuleInner = ({ module, scope, runtime, publicPath, LoadingComponen
|
|
|
82
127
|
(_runtime = runtime) === null || _runtime === void 0 ? void 0 : _runtime.react,
|
|
83
128
|
(_runtime1 = runtime) === null || _runtime1 === void 0 ? void 0 : _runtime1.reactDOM,
|
|
84
129
|
resolvedMountNode,
|
|
85
|
-
fallbackContainerClassName
|
|
130
|
+
fallbackContainerClassName,
|
|
131
|
+
enhancedPluginManager,
|
|
132
|
+
componentProps
|
|
86
133
|
]);
|
|
87
134
|
const Loading = LoadingComponent || /*#__PURE__*/ React.createElement("div", null, "Loading...");
|
|
88
135
|
const ErrorFallback = ({ error })=>ErrorComponent || /*#__PURE__*/ React.createElement("div", null, "远程模块加载失败: ", error.message);
|
|
89
136
|
if (!Component) return Loading;
|
|
137
|
+
// 应用增强插件的属性注入
|
|
138
|
+
const injectedProps = enhancedPluginManager.injectProps(componentProps || {}, {
|
|
139
|
+
remoteName: scope,
|
|
140
|
+
moduleName: module
|
|
141
|
+
});
|
|
90
142
|
return /*#__PURE__*/ React.createElement(ErrorBoundary, {
|
|
91
143
|
resetKeys: [
|
|
92
144
|
module,
|
|
@@ -98,8 +150,8 @@ const RemoteModuleInner = ({ module, scope, runtime, publicPath, LoadingComponen
|
|
|
98
150
|
}, /*#__PURE__*/ React.createElement(React.Suspense, {
|
|
99
151
|
fallback: Loading
|
|
100
152
|
}, /*#__PURE__*/ React.createElement(Component, _object_spread({
|
|
101
|
-
ref
|
|
102
|
-
},
|
|
153
|
+
ref
|
|
154
|
+
}, injectedProps), children)));
|
|
103
155
|
};
|
|
104
156
|
// 使用 forwardRef 包装组件以支持 ref
|
|
105
157
|
export const RemoteModule = /*#__PURE__*/ forwardRef(RemoteModuleInner);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { _ as _object_spread } from "@swc/helpers/_/_object_spread";
|
|
2
|
+
import { _ as _object_spread_props } from "@swc/helpers/_/_object_spread_props";
|
|
3
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
4
|
+
import { EnhancedPluginManager, getEnhancedPluginManager, registerEnhancedPlugins } from '../plugin-manager';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
describe('EnhancedPluginManager', ()=>{
|
|
7
|
+
let manager;
|
|
8
|
+
beforeEach(()=>{
|
|
9
|
+
manager = new EnhancedPluginManager();
|
|
10
|
+
});
|
|
11
|
+
describe('Plugin Registration', ()=>{
|
|
12
|
+
it('should register an enhanced plugin', ()=>{
|
|
13
|
+
const enhancedPlugin = {
|
|
14
|
+
name: 'test-enhanced',
|
|
15
|
+
wrapComponent: (Component)=>Component,
|
|
16
|
+
injectProps: (props)=>props
|
|
17
|
+
};
|
|
18
|
+
manager.register(enhancedPlugin);
|
|
19
|
+
const plugins = manager.getPlugins();
|
|
20
|
+
expect(plugins).toHaveLength(1);
|
|
21
|
+
expect(plugins[0].name).toBe('test-enhanced');
|
|
22
|
+
});
|
|
23
|
+
it('should replace existing plugin with same name', ()=>{
|
|
24
|
+
const plugin1 = {
|
|
25
|
+
name: 'test-plugin',
|
|
26
|
+
wrapComponent: (Component)=>Component
|
|
27
|
+
};
|
|
28
|
+
const plugin2 = {
|
|
29
|
+
name: 'test-plugin',
|
|
30
|
+
injectProps: (props)=>props
|
|
31
|
+
};
|
|
32
|
+
manager.register(plugin1);
|
|
33
|
+
manager.register(plugin2);
|
|
34
|
+
const plugins = manager.getPlugins();
|
|
35
|
+
expect(plugins).toHaveLength(1);
|
|
36
|
+
expect(plugins[0]).toBe(plugin2);
|
|
37
|
+
});
|
|
38
|
+
it('should register multiple plugins', ()=>{
|
|
39
|
+
const plugins = [
|
|
40
|
+
{
|
|
41
|
+
name: 'plugin1',
|
|
42
|
+
wrapComponent: (Component)=>Component
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'plugin2',
|
|
46
|
+
injectProps: (props)=>props
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
manager.registerPlugins(plugins);
|
|
50
|
+
expect(manager.getPlugins()).toHaveLength(2);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('Component Wrapping', ()=>{
|
|
54
|
+
it('should wrap component with single plugin', ()=>{
|
|
55
|
+
const TestComponent = ()=>React.createElement('div', null, 'test');
|
|
56
|
+
const mockWrapper = vi.fn((Component)=>Component);
|
|
57
|
+
const plugin = {
|
|
58
|
+
name: 'wrapper-plugin',
|
|
59
|
+
wrapComponent: mockWrapper
|
|
60
|
+
};
|
|
61
|
+
manager.register(plugin);
|
|
62
|
+
const context = {
|
|
63
|
+
remoteName: 'test-remote',
|
|
64
|
+
moduleName: 'test-module',
|
|
65
|
+
props: {
|
|
66
|
+
test: true
|
|
67
|
+
},
|
|
68
|
+
React: React,
|
|
69
|
+
ReactDOM: undefined
|
|
70
|
+
};
|
|
71
|
+
const wrappedComponent = manager.wrapComponent(TestComponent, context);
|
|
72
|
+
expect(mockWrapper).toHaveBeenCalledWith(TestComponent, context);
|
|
73
|
+
expect(wrappedComponent).toBe(TestComponent);
|
|
74
|
+
});
|
|
75
|
+
it('should apply multiple wrappers in order', ()=>{
|
|
76
|
+
const TestComponent = ()=>React.createElement('div', null, 'original');
|
|
77
|
+
const Wrapper1 = ()=>React.createElement('div', null, 'wrapper1');
|
|
78
|
+
const Wrapper2 = ()=>React.createElement('div', null, 'wrapper2');
|
|
79
|
+
const plugin1 = {
|
|
80
|
+
name: 'wrapper1',
|
|
81
|
+
wrapComponent: ()=>Wrapper1
|
|
82
|
+
};
|
|
83
|
+
const plugin2 = {
|
|
84
|
+
name: 'wrapper2',
|
|
85
|
+
wrapComponent: ()=>Wrapper2
|
|
86
|
+
};
|
|
87
|
+
// 注册顺序很重要
|
|
88
|
+
manager.register(plugin1);
|
|
89
|
+
manager.register(plugin2);
|
|
90
|
+
const context = {
|
|
91
|
+
remoteName: 'test-remote',
|
|
92
|
+
moduleName: 'test-module',
|
|
93
|
+
props: {},
|
|
94
|
+
React: React,
|
|
95
|
+
ReactDOM: undefined
|
|
96
|
+
};
|
|
97
|
+
const result = manager.wrapComponent(TestComponent, context);
|
|
98
|
+
// plugin2 应该最后执行,所以返回 Wrapper2
|
|
99
|
+
expect(result).toBe(Wrapper2);
|
|
100
|
+
});
|
|
101
|
+
it('should handle wrapper errors gracefully', ()=>{
|
|
102
|
+
const TestComponent = ()=>React.createElement('div', null, 'test');
|
|
103
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(()=>{});
|
|
104
|
+
const plugin = {
|
|
105
|
+
name: 'error-plugin',
|
|
106
|
+
wrapComponent: ()=>{
|
|
107
|
+
throw new Error('Wrapper error');
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
manager.register(plugin);
|
|
111
|
+
const context = {
|
|
112
|
+
remoteName: 'test-remote',
|
|
113
|
+
moduleName: 'test-module',
|
|
114
|
+
props: {},
|
|
115
|
+
React: React,
|
|
116
|
+
ReactDOM: undefined
|
|
117
|
+
};
|
|
118
|
+
const result = manager.wrapComponent(TestComponent, context);
|
|
119
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Error applying wrapper from plugin "error-plugin"'), expect.any(Error));
|
|
120
|
+
expect(result).toBe(TestComponent); // 应该返回原始组件
|
|
121
|
+
consoleSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('Props Injection', ()=>{
|
|
125
|
+
it('should inject props with single plugin', ()=>{
|
|
126
|
+
const mockInjector = vi.fn((props)=>_object_spread_props(_object_spread({}, props), {
|
|
127
|
+
injected: true
|
|
128
|
+
}));
|
|
129
|
+
const plugin = {
|
|
130
|
+
name: 'injector-plugin',
|
|
131
|
+
injectProps: mockInjector
|
|
132
|
+
};
|
|
133
|
+
manager.register(plugin);
|
|
134
|
+
const context = {
|
|
135
|
+
remoteName: 'test-remote',
|
|
136
|
+
moduleName: 'test-module'
|
|
137
|
+
};
|
|
138
|
+
const result = manager.injectProps({
|
|
139
|
+
original: true
|
|
140
|
+
}, context);
|
|
141
|
+
expect(mockInjector).toHaveBeenCalledWith({
|
|
142
|
+
original: true
|
|
143
|
+
}, context);
|
|
144
|
+
expect(result).toEqual({
|
|
145
|
+
original: true,
|
|
146
|
+
injected: true
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it('should apply multiple injectors in order', ()=>{
|
|
150
|
+
const plugin1 = {
|
|
151
|
+
name: 'injector1',
|
|
152
|
+
injectProps: (props)=>_object_spread_props(_object_spread({}, props), {
|
|
153
|
+
step1: true
|
|
154
|
+
})
|
|
155
|
+
};
|
|
156
|
+
const plugin2 = {
|
|
157
|
+
name: 'injector2',
|
|
158
|
+
injectProps: (props)=>_object_spread_props(_object_spread({}, props), {
|
|
159
|
+
step2: true
|
|
160
|
+
})
|
|
161
|
+
};
|
|
162
|
+
manager.register(plugin1);
|
|
163
|
+
manager.register(plugin2);
|
|
164
|
+
const context = {
|
|
165
|
+
remoteName: 'test-remote',
|
|
166
|
+
moduleName: 'test-module'
|
|
167
|
+
};
|
|
168
|
+
const result = manager.injectProps({
|
|
169
|
+
original: true
|
|
170
|
+
}, context);
|
|
171
|
+
expect(result).toEqual({
|
|
172
|
+
original: true,
|
|
173
|
+
step1: true,
|
|
174
|
+
step2: true
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
it('should handle injection errors gracefully', ()=>{
|
|
178
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(()=>{});
|
|
179
|
+
const plugin = {
|
|
180
|
+
name: 'error-plugin',
|
|
181
|
+
injectProps: ()=>{
|
|
182
|
+
throw new Error('Injection error');
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
manager.register(plugin);
|
|
186
|
+
const context = {
|
|
187
|
+
remoteName: 'test-remote',
|
|
188
|
+
moduleName: 'test-module'
|
|
189
|
+
};
|
|
190
|
+
const result = manager.injectProps({
|
|
191
|
+
original: true
|
|
192
|
+
}, context);
|
|
193
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Error injecting props from plugin "error-plugin"'), expect.any(Error));
|
|
194
|
+
expect(result).toEqual({
|
|
195
|
+
original: true
|
|
196
|
+
}); // 应该返回原始props
|
|
197
|
+
consoleSpy.mockRestore();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('Plugin Management', ()=>{
|
|
201
|
+
it('should get plugin info', ()=>{
|
|
202
|
+
const wrapperPlugin = {
|
|
203
|
+
name: 'wrapper-plugin',
|
|
204
|
+
wrapComponent: (Component)=>Component
|
|
205
|
+
};
|
|
206
|
+
const injectorPlugin = {
|
|
207
|
+
name: 'injector-plugin',
|
|
208
|
+
injectProps: (props)=>props
|
|
209
|
+
};
|
|
210
|
+
const hybridPlugin = {
|
|
211
|
+
name: 'hybrid-plugin',
|
|
212
|
+
wrapComponent: (Component)=>Component,
|
|
213
|
+
injectProps: (props)=>props
|
|
214
|
+
};
|
|
215
|
+
manager.register(wrapperPlugin);
|
|
216
|
+
manager.register(injectorPlugin);
|
|
217
|
+
manager.register(hybridPlugin);
|
|
218
|
+
const info = manager.getPluginInfo();
|
|
219
|
+
expect(info).toHaveLength(3);
|
|
220
|
+
expect(info[0]).toEqual({
|
|
221
|
+
name: 'wrapper-plugin',
|
|
222
|
+
hasWrapper: true,
|
|
223
|
+
hasInjector: false
|
|
224
|
+
});
|
|
225
|
+
expect(info[1]).toEqual({
|
|
226
|
+
name: 'injector-plugin',
|
|
227
|
+
hasWrapper: false,
|
|
228
|
+
hasInjector: true
|
|
229
|
+
});
|
|
230
|
+
expect(info[2]).toEqual({
|
|
231
|
+
name: 'hybrid-plugin',
|
|
232
|
+
hasWrapper: true,
|
|
233
|
+
hasInjector: true
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it('should remove plugin', ()=>{
|
|
237
|
+
const plugin = {
|
|
238
|
+
name: 'test-plugin',
|
|
239
|
+
wrapComponent: (Component)=>Component
|
|
240
|
+
};
|
|
241
|
+
manager.register(plugin);
|
|
242
|
+
expect(manager.getPlugins()).toHaveLength(1);
|
|
243
|
+
const removed = manager.removePlugin('test-plugin');
|
|
244
|
+
expect(removed).toBe(true);
|
|
245
|
+
expect(manager.getPlugins()).toHaveLength(0);
|
|
246
|
+
});
|
|
247
|
+
it('should return false when removing non-existent plugin', ()=>{
|
|
248
|
+
const removed = manager.removePlugin('non-existent');
|
|
249
|
+
expect(removed).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
it('should clear all plugins', ()=>{
|
|
252
|
+
manager.register({
|
|
253
|
+
name: 'plugin1',
|
|
254
|
+
wrapComponent: (Component)=>Component
|
|
255
|
+
});
|
|
256
|
+
manager.register({
|
|
257
|
+
name: 'plugin2',
|
|
258
|
+
injectProps: (props)=>props
|
|
259
|
+
});
|
|
260
|
+
expect(manager.getPlugins()).toHaveLength(2);
|
|
261
|
+
manager.clear();
|
|
262
|
+
expect(manager.getPlugins()).toHaveLength(0);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe('Global Enhanced Plugin Manager', ()=>{
|
|
266
|
+
beforeEach(()=>{
|
|
267
|
+
// 清理全局状态
|
|
268
|
+
getEnhancedPluginManager().clear();
|
|
269
|
+
});
|
|
270
|
+
it('should provide singleton instance', ()=>{
|
|
271
|
+
const manager1 = getEnhancedPluginManager();
|
|
272
|
+
const manager2 = getEnhancedPluginManager();
|
|
273
|
+
expect(manager1).toBe(manager2);
|
|
274
|
+
});
|
|
275
|
+
it('should register multiple plugins via convenience function', ()=>{
|
|
276
|
+
const plugins = [
|
|
277
|
+
{
|
|
278
|
+
name: 'plugin1',
|
|
279
|
+
wrapComponent: (Component)=>Component
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'plugin2',
|
|
283
|
+
injectProps: (props)=>props
|
|
284
|
+
}
|
|
285
|
+
];
|
|
286
|
+
registerEnhancedPlugins(plugins);
|
|
287
|
+
const manager = getEnhancedPluginManager();
|
|
288
|
+
expect(manager.getPlugins()).toHaveLength(2);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|