@module-federation/bridge-react 0.0.0-docs-remove-invalid-lark-link-20251205062649
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/CHANGELOG.md +728 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/__tests__/bridge.spec.tsx +160 -0
- package/__tests__/createLazyComponent.spec.tsx +209 -0
- package/__tests__/prefetch.spec.ts +156 -0
- package/__tests__/router.spec.tsx +82 -0
- package/__tests__/setupTests.ts +8 -0
- package/__tests__/util.ts +36 -0
- package/dist/base.cjs.js +29 -0
- package/dist/base.d.ts +311 -0
- package/dist/base.es.js +30 -0
- package/dist/bridge-base-CPSTBjEp.mjs +211 -0
- package/dist/bridge-base-RStDxH71.js +226 -0
- package/dist/createHelpers-B_L612IN.js +190 -0
- package/dist/createHelpers-Ui5pt7je.mjs +191 -0
- package/dist/data-fetch-server-middleware.cjs.js +163 -0
- package/dist/data-fetch-server-middleware.d.ts +15 -0
- package/dist/data-fetch-server-middleware.es.js +164 -0
- package/dist/data-fetch-utils.cjs.js +24 -0
- package/dist/data-fetch-utils.d.ts +81 -0
- package/dist/data-fetch-utils.es.js +26 -0
- package/dist/index-DRSBaSu3.js +45 -0
- package/dist/index-DyQNwY2M.mjs +46 -0
- package/dist/index.cjs.js +125 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.es.js +109 -0
- package/dist/index.esm-BWaKho-8.js +491 -0
- package/dist/index.esm-CPwSeCvw.mjs +492 -0
- package/dist/lazy-load-component-plugin-CSRkMmKF.js +521 -0
- package/dist/lazy-load-component-plugin-DXqhuywC.mjs +522 -0
- package/dist/lazy-load-component-plugin.cjs.js +6 -0
- package/dist/lazy-load-component-plugin.d.ts +16 -0
- package/dist/lazy-load-component-plugin.es.js +6 -0
- package/dist/lazy-utils.cjs.js +24 -0
- package/dist/lazy-utils.d.ts +149 -0
- package/dist/lazy-utils.es.js +24 -0
- package/dist/plugin.cjs.js +14 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.es.js +14 -0
- package/dist/prefetch-A3QkU5oZ.js +1272 -0
- package/dist/prefetch-zMJL79zx.mjs +1273 -0
- package/dist/router-v5.cjs.js +55 -0
- package/dist/router-v5.d.ts +18 -0
- package/dist/router-v5.es.js +32 -0
- package/dist/router-v6.cjs.js +84 -0
- package/dist/router-v6.d.ts +20 -0
- package/dist/router-v6.es.js +61 -0
- package/dist/router-v7.cjs.js +83 -0
- package/dist/router-v7.d.ts +20 -0
- package/dist/router-v7.es.js +61 -0
- package/dist/router.cjs.js +82 -0
- package/dist/router.d.ts +20 -0
- package/dist/router.es.js +60 -0
- package/dist/utils-dUgb9Jkm.mjs +2016 -0
- package/dist/utils-tM9yE73c.js +2015 -0
- package/dist/v18.cjs.js +15 -0
- package/dist/v18.d.ts +114 -0
- package/dist/v18.es.js +15 -0
- package/dist/v19.cjs.js +15 -0
- package/dist/v19.d.ts +115 -0
- package/dist/v19.es.js +15 -0
- package/jest.config.ts +21 -0
- package/package.json +173 -0
- package/project.json +23 -0
- package/src/base.ts +50 -0
- package/src/index.ts +50 -0
- package/src/lazy/AwaitDataFetch.tsx +215 -0
- package/src/lazy/constant.ts +30 -0
- package/src/lazy/createLazyComponent.tsx +411 -0
- package/src/lazy/data-fetch/cache.ts +291 -0
- package/src/lazy/data-fetch/call-data-fetch.ts +13 -0
- package/src/lazy/data-fetch/data-fetch-server-middleware.ts +196 -0
- package/src/lazy/data-fetch/index.ts +16 -0
- package/src/lazy/data-fetch/inject-data-fetch.ts +109 -0
- package/src/lazy/data-fetch/prefetch.ts +112 -0
- package/src/lazy/data-fetch/runtime-plugin.ts +115 -0
- package/src/lazy/index.ts +35 -0
- package/src/lazy/logger.ts +6 -0
- package/src/lazy/types.ts +75 -0
- package/src/lazy/utils.ts +372 -0
- package/src/lazy/wrapNoSSR.tsx +10 -0
- package/src/modern-app-env.d.ts +2 -0
- package/src/plugins/lazy-load-component-plugin.spec.ts +21 -0
- package/src/plugins/lazy-load-component-plugin.ts +57 -0
- package/src/provider/context.tsx +4 -0
- package/src/provider/plugin.ts +22 -0
- package/src/provider/versions/bridge-base.tsx +150 -0
- package/src/provider/versions/legacy.ts +87 -0
- package/src/provider/versions/v18.ts +47 -0
- package/src/provider/versions/v19.ts +48 -0
- package/src/remote/RemoteAppWrapper.tsx +108 -0
- package/src/remote/base-component/component.tsx +2 -0
- package/src/remote/base-component/create.tsx +23 -0
- package/src/remote/base-component/index.tsx +10 -0
- package/src/remote/createHelpers.tsx +130 -0
- package/src/remote/router-component/component.tsx +104 -0
- package/src/remote/router-component/create.tsx +23 -0
- package/src/remote/router-component/index.tsx +10 -0
- package/src/router/default.tsx +73 -0
- package/src/router/v5.tsx +43 -0
- package/src/router/v6.tsx +74 -0
- package/src/router/v7.tsx +75 -0
- package/src/types.ts +147 -0
- package/src/utils/index.ts +44 -0
- package/src/v18.ts +9 -0
- package/src/v19.ts +9 -0
- package/tsconfig.json +42 -0
- package/tsconfig.node.json +11 -0
- package/tsconfig.spec.json +26 -0
- package/vite.config.ts +112 -0
- package/vitest.config.ts +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 ScriptedAlchemy LLC (Zack Jackson) Zhou Shaw (zhouxiao)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# React Bridge
|
|
2
|
+
|
|
3
|
+
React bridge is used to load the routing module in mf, so that the routing module can work properly with the host environment.
|
|
4
|
+
|
|
5
|
+
> When to use
|
|
6
|
+
|
|
7
|
+
- Load the route module
|
|
8
|
+
- Load across the front end framework
|
|
9
|
+
|
|
10
|
+
## How to use
|
|
11
|
+
|
|
12
|
+
# 1. Install the react bridge library
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @module-federation/bridge-react
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
# 2. Configure the react bridge library
|
|
19
|
+
|
|
20
|
+
> Use createBridgeComponent create component provider
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
// ./src/index.tsx
|
|
24
|
+
import { createBridgeComponent } from '@module-federation/bridge-react';
|
|
25
|
+
|
|
26
|
+
function App() {
|
|
27
|
+
return ( <BrowserRouter basename="/">
|
|
28
|
+
<Routes>
|
|
29
|
+
<Route path="/" Component={()=> <div>Home page</div>}>
|
|
30
|
+
<Route path="/detail" Component={()=> <div>Detail page</div>}>
|
|
31
|
+
</Routes>
|
|
32
|
+
</BrowserRouter>)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default createBridgeComponent({
|
|
36
|
+
rootComponent: App
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> set alias to proxy
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
//rsbuild.config.ts
|
|
44
|
+
export default defineConfig({
|
|
45
|
+
source: {
|
|
46
|
+
alias: {
|
|
47
|
+
'react-router-dom$': path.resolve(
|
|
48
|
+
__dirname,
|
|
49
|
+
'node_modules/@module-federation/bridge-react/dist/router.es.js',
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
server: {
|
|
54
|
+
port: 2001,
|
|
55
|
+
host: 'localhost',
|
|
56
|
+
},
|
|
57
|
+
dev: {
|
|
58
|
+
assetPrefix: 'http://localhost:2001',
|
|
59
|
+
},
|
|
60
|
+
tools: {
|
|
61
|
+
rspack: (config, { appendPlugins }) => {
|
|
62
|
+
delete config.optimization?.splitChunks;
|
|
63
|
+
config.output!.uniqueName = 'remote1';
|
|
64
|
+
appendPlugins([
|
|
65
|
+
new ModuleFederationPlugin({
|
|
66
|
+
name: 'remote1',
|
|
67
|
+
exposes: {
|
|
68
|
+
'./export-app': './src/index.tsx',
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
# 3. Load the module with routing
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
//rsbuild.config.ts
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
tools: {
|
|
83
|
+
rspack: (config, { appendPlugins }) => {
|
|
84
|
+
config.output!.uniqueName = 'host';
|
|
85
|
+
appendPlugins([
|
|
86
|
+
new ModuleFederationPlugin({
|
|
87
|
+
name: 'host',
|
|
88
|
+
remotes: {
|
|
89
|
+
remote1: 'remote1@http://localhost:2001/mf-manifest.json',
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> Use the module
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
// ./src/index.tsx
|
|
102
|
+
import { createBridgeComponent } from '@module-federation/bridge-react';
|
|
103
|
+
|
|
104
|
+
const Remote1 = createBridgeComponent(()=> import('remote1/export-app'));
|
|
105
|
+
|
|
106
|
+
function App() {
|
|
107
|
+
return ( <BrowserRouter basename="/">
|
|
108
|
+
<ul>
|
|
109
|
+
<li>
|
|
110
|
+
<Link to="/">
|
|
111
|
+
Home
|
|
112
|
+
</Link>
|
|
113
|
+
</li>
|
|
114
|
+
<li>
|
|
115
|
+
<Link to="/remote1">
|
|
116
|
+
Remote1
|
|
117
|
+
</Link>
|
|
118
|
+
</li>
|
|
119
|
+
</ul>
|
|
120
|
+
<Routes>
|
|
121
|
+
<Route path="/" Component={()=> <div>Home page</div>}>
|
|
122
|
+
<Route path="/remote1" Component={()=> <Remote1 />}>
|
|
123
|
+
</Routes>
|
|
124
|
+
</BrowserRouter>)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
128
|
+
root.render(
|
|
129
|
+
<App />
|
|
130
|
+
);
|
|
131
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createBridgeComponent, createRemoteAppComponent } from '../src';
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from '@testing-library/react';
|
|
10
|
+
import { createContainer, getHtml } from './util';
|
|
11
|
+
|
|
12
|
+
describe('bridge', () => {
|
|
13
|
+
let containerInfo: ReturnType<typeof createContainer>;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
containerInfo = createContainer();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
containerInfo?.clean();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('createBridgeComponent life cycle', async () => {
|
|
23
|
+
function Component() {
|
|
24
|
+
return <div>life cycle render</div>;
|
|
25
|
+
}
|
|
26
|
+
const lifeCycle = createBridgeComponent({
|
|
27
|
+
rootComponent: Component,
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
lifeCycle.render({
|
|
31
|
+
dom: containerInfo?.container,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await waitFor(
|
|
35
|
+
() => {
|
|
36
|
+
expect(document.querySelector('#container')?.innerHTML).toContain(
|
|
37
|
+
'<div>life cycle render</div>',
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
{ timeout: 2000 },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
lifeCycle.destroy({
|
|
44
|
+
dom: containerInfo?.container,
|
|
45
|
+
moduleName: 'test',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await waitFor(
|
|
49
|
+
() => {
|
|
50
|
+
expect(
|
|
51
|
+
(document.querySelector('#container')?.innerHTML || '').trim(),
|
|
52
|
+
).toBe('');
|
|
53
|
+
},
|
|
54
|
+
{ timeout: 2000 },
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('createRemoteAppComponent', async () => {
|
|
59
|
+
function Component({ props }: { props?: Record<string, any> }) {
|
|
60
|
+
return <div>life cycle render {props?.msg}</div>;
|
|
61
|
+
}
|
|
62
|
+
const BridgeComponent = createBridgeComponent({
|
|
63
|
+
rootComponent: Component,
|
|
64
|
+
});
|
|
65
|
+
const RemoteComponent = createRemoteAppComponent({
|
|
66
|
+
loader: async () => {
|
|
67
|
+
return {
|
|
68
|
+
default: BridgeComponent,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
fallback: () => <div></div>,
|
|
72
|
+
loading: <div>loading</div>,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<RemoteComponent props={{ msg: 'hello world' }} />,
|
|
77
|
+
);
|
|
78
|
+
expect(getHtml(container)).toMatch('loading');
|
|
79
|
+
|
|
80
|
+
await waitFor(
|
|
81
|
+
() => {
|
|
82
|
+
expect(getHtml(container)).toMatch('life cycle render');
|
|
83
|
+
expect(getHtml(container)).toMatch('hello world');
|
|
84
|
+
},
|
|
85
|
+
{ timeout: 2000 },
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('createRemoteAppComponent and obtain ref property', async () => {
|
|
90
|
+
const ref = {
|
|
91
|
+
current: null,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function Component({ props }: { props?: Record<string, any> }) {
|
|
95
|
+
return <div>life cycle render {props?.msg}</div>;
|
|
96
|
+
}
|
|
97
|
+
const BridgeComponent = createBridgeComponent({
|
|
98
|
+
rootComponent: Component,
|
|
99
|
+
});
|
|
100
|
+
const RemoteComponent = createRemoteAppComponent({
|
|
101
|
+
loader: async () => {
|
|
102
|
+
return {
|
|
103
|
+
default: BridgeComponent,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
fallback: () => <div></div>,
|
|
107
|
+
loading: <div>loading</div>,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<RemoteComponent ref={ref} props={{ msg: 'hello world' }} />,
|
|
112
|
+
);
|
|
113
|
+
expect(getHtml(container)).toMatch('loading');
|
|
114
|
+
|
|
115
|
+
await waitFor(
|
|
116
|
+
() => {
|
|
117
|
+
expect(getHtml(container)).toMatch('life cycle render');
|
|
118
|
+
expect(getHtml(container)).toMatch('hello world');
|
|
119
|
+
expect(ref.current).not.toBeNull();
|
|
120
|
+
},
|
|
121
|
+
{ timeout: 2000 },
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('createRemoteAppComponent with custom createRoot prop', async () => {
|
|
126
|
+
const renderMock = jest.fn();
|
|
127
|
+
|
|
128
|
+
function Component({ props }: { props?: Record<string, any> }) {
|
|
129
|
+
return <div>life cycle render {props?.msg}</div>;
|
|
130
|
+
}
|
|
131
|
+
const BridgeComponent = createBridgeComponent({
|
|
132
|
+
rootComponent: Component,
|
|
133
|
+
createRoot: () => {
|
|
134
|
+
return {
|
|
135
|
+
render: renderMock,
|
|
136
|
+
unmount: jest.fn(),
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
const RemoteComponent = createRemoteAppComponent({
|
|
141
|
+
loader: async () => {
|
|
142
|
+
return {
|
|
143
|
+
default: BridgeComponent,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
fallback: () => <div></div>,
|
|
147
|
+
loading: <div>loading</div>,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { container } = render(<RemoteComponent />);
|
|
151
|
+
expect(getHtml(container)).toMatch('loading');
|
|
152
|
+
|
|
153
|
+
await waitFor(
|
|
154
|
+
() => {
|
|
155
|
+
expect(renderMock).toHaveBeenCalledTimes(1);
|
|
156
|
+
},
|
|
157
|
+
{ timeout: 2000 },
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { Suspense } from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import {
|
|
4
|
+
createLazyComponent,
|
|
5
|
+
collectSSRAssets,
|
|
6
|
+
} from '../src/lazy/createLazyComponent';
|
|
7
|
+
import * as runtime from '@module-federation/runtime';
|
|
8
|
+
import * as utils from '../src/lazy/utils';
|
|
9
|
+
|
|
10
|
+
// Mocking dependencies
|
|
11
|
+
jest.mock('@module-federation/runtime');
|
|
12
|
+
jest.mock('../src/lazy/utils');
|
|
13
|
+
|
|
14
|
+
const mockGetInstance = runtime.getInstance as jest.Mock;
|
|
15
|
+
const mockGetLoadedRemoteInfos = utils.getLoadedRemoteInfos as jest.Mock;
|
|
16
|
+
const mockGetDataFetchMapKey = utils.getDataFetchMapKey as jest.Mock;
|
|
17
|
+
const mockFetchData = utils.fetchData as jest.Mock;
|
|
18
|
+
|
|
19
|
+
const MockComponent = () => <div>Mock Component</div>;
|
|
20
|
+
const LoadingComponent = () => <div>Loading...</div>;
|
|
21
|
+
const ErrorComponent = () => <div>Error!</div>;
|
|
22
|
+
|
|
23
|
+
describe('createLazyComponent', () => {
|
|
24
|
+
let mockInstance: any;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
mockInstance = {
|
|
29
|
+
name: 'host-app',
|
|
30
|
+
options: { version: '1.0.0' },
|
|
31
|
+
getModuleInfo: jest.fn(),
|
|
32
|
+
};
|
|
33
|
+
mockGetInstance.mockReturnValue(mockInstance);
|
|
34
|
+
mockGetLoadedRemoteInfos.mockReturnValue({
|
|
35
|
+
name: 'remoteApp',
|
|
36
|
+
alias: 'remote',
|
|
37
|
+
expose: './Component',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
snapshot: {
|
|
40
|
+
modules: [
|
|
41
|
+
{
|
|
42
|
+
modulePath: './Component',
|
|
43
|
+
assets: {
|
|
44
|
+
css: { sync: [], async: [] },
|
|
45
|
+
js: { sync: [], async: [] },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
publicPath: 'http://localhost:3001/',
|
|
50
|
+
remoteEntry: 'remoteEntry.js',
|
|
51
|
+
},
|
|
52
|
+
entryGlobalName: 'remoteApp',
|
|
53
|
+
});
|
|
54
|
+
mockGetDataFetchMapKey.mockReturnValue('data-fetch-key');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should render loading component then the actual component', async () => {
|
|
58
|
+
const loader = jest.fn().mockResolvedValue({
|
|
59
|
+
default: MockComponent,
|
|
60
|
+
[Symbol.for('mf_module_id')]: 'remoteApp/Component',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const LazyComponent = createLazyComponent({
|
|
64
|
+
loader,
|
|
65
|
+
instance: mockInstance,
|
|
66
|
+
loading: <LoadingComponent />,
|
|
67
|
+
fallback: <ErrorComponent />,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
<Suspense fallback={<LoadingComponent />}>
|
|
72
|
+
<LazyComponent />
|
|
73
|
+
</Suspense>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(screen.getByText('Mock Component')).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should render fallback component on data fetch error', async () => {
|
|
84
|
+
mockFetchData.mockRejectedValue(new Error('Data fetch failed'));
|
|
85
|
+
const LazyComponentWithDataFetch = createLazyComponent({
|
|
86
|
+
loader: jest.fn().mockResolvedValue({
|
|
87
|
+
default: MockComponent,
|
|
88
|
+
[Symbol.for('mf_module_id')]: 'remoteApp/Component',
|
|
89
|
+
}),
|
|
90
|
+
instance: mockInstance,
|
|
91
|
+
loading: <LoadingComponent />,
|
|
92
|
+
fallback: <ErrorComponent />,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
render(<LazyComponentWithDataFetch />);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(screen.getByText('Error!')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should fetch data and pass it to the component', async () => {
|
|
103
|
+
const loader = jest.fn().mockResolvedValue({
|
|
104
|
+
default: (props: { mfData: any }) => (
|
|
105
|
+
<div>Data: {JSON.stringify(props.mfData)}</div>
|
|
106
|
+
),
|
|
107
|
+
[Symbol.for('mf_module_id')]: 'remoteApp/Component',
|
|
108
|
+
});
|
|
109
|
+
const mockData = { message: 'Hello' };
|
|
110
|
+
mockFetchData.mockResolvedValue(mockData);
|
|
111
|
+
|
|
112
|
+
const LazyComponent = createLazyComponent({
|
|
113
|
+
loader,
|
|
114
|
+
instance: mockInstance,
|
|
115
|
+
loading: <LoadingComponent />,
|
|
116
|
+
fallback: <ErrorComponent />,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
render(<LazyComponent />);
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(
|
|
123
|
+
screen.getByText(`Data: ${JSON.stringify(mockData)}`),
|
|
124
|
+
).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('collectSSRAssets', () => {
|
|
130
|
+
let mockInstance: any;
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
jest.clearAllMocks();
|
|
134
|
+
mockInstance = {
|
|
135
|
+
name: 'host-app',
|
|
136
|
+
options: { version: '1.0.0' },
|
|
137
|
+
};
|
|
138
|
+
mockGetInstance.mockReturnValue(mockInstance);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return an empty array if instance is not available', () => {
|
|
142
|
+
const assets = collectSSRAssets({
|
|
143
|
+
id: 'test/expose',
|
|
144
|
+
instance: undefined as any,
|
|
145
|
+
});
|
|
146
|
+
expect(assets).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return an empty array if module info is not found', () => {
|
|
150
|
+
mockGetLoadedRemoteInfos.mockReturnValue(undefined);
|
|
151
|
+
const assets = collectSSRAssets({
|
|
152
|
+
id: 'test/expose',
|
|
153
|
+
instance: mockInstance,
|
|
154
|
+
});
|
|
155
|
+
expect(assets).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should collect CSS and JS assets for SSR', () => {
|
|
159
|
+
mockGetLoadedRemoteInfos.mockReturnValue({
|
|
160
|
+
name: 'remoteApp',
|
|
161
|
+
expose: './Component',
|
|
162
|
+
snapshot: {
|
|
163
|
+
publicPath: 'http://localhost:3001/',
|
|
164
|
+
remoteEntry: 'remoteEntry.js',
|
|
165
|
+
modules: [
|
|
166
|
+
{
|
|
167
|
+
modulePath: './Component',
|
|
168
|
+
assets: {
|
|
169
|
+
css: { sync: ['main.css'], async: ['extra.css'] },
|
|
170
|
+
js: { sync: ['main.js'], async: [] },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const assets = collectSSRAssets({
|
|
178
|
+
id: 'remoteApp/Component',
|
|
179
|
+
instance: mockInstance,
|
|
180
|
+
injectScript: true,
|
|
181
|
+
injectLink: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(assets).toHaveLength(4); // 2 links, 2 scripts
|
|
185
|
+
|
|
186
|
+
const links = assets.filter(
|
|
187
|
+
(asset) => (asset as React.ReactElement).type === 'link',
|
|
188
|
+
);
|
|
189
|
+
const scripts = assets.filter(
|
|
190
|
+
(asset) => (asset as React.ReactElement).type === 'script',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(links).toHaveLength(2);
|
|
194
|
+
expect((links[0] as React.ReactElement).props.href).toBe(
|
|
195
|
+
'http://localhost:3001/extra.css',
|
|
196
|
+
);
|
|
197
|
+
expect((links[1] as React.ReactElement).props.href).toBe(
|
|
198
|
+
'http://localhost:3001/main.css',
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(scripts).toHaveLength(2);
|
|
202
|
+
expect((scripts[0] as React.ReactElement).props.src).toBe(
|
|
203
|
+
'http://localhost:3001/remoteEntry.js',
|
|
204
|
+
);
|
|
205
|
+
expect((scripts[1] as React.ReactElement).props.src).toBe(
|
|
206
|
+
'http://localhost:3001/main.js',
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { prefetch } from '../src/lazy/data-fetch/prefetch';
|
|
2
|
+
import * as utils from '../src/lazy/utils';
|
|
3
|
+
import logger from '../src/lazy/logger';
|
|
4
|
+
import helpers from '@module-federation/runtime/helpers';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
jest.mock('../src/lazy/logger');
|
|
8
|
+
jest.mock('../src/lazy/utils');
|
|
9
|
+
jest.mock('@module-federation/runtime/helpers', () => ({
|
|
10
|
+
default: {
|
|
11
|
+
utils: {
|
|
12
|
+
matchRemoteWithNameAndExpose: jest.fn(),
|
|
13
|
+
getRemoteInfo: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
utils: {
|
|
17
|
+
matchRemoteWithNameAndExpose: jest.fn(),
|
|
18
|
+
getRemoteInfo: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('prefetch', () => {
|
|
23
|
+
let mockInstance: any;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
mockInstance = {
|
|
28
|
+
name: 'host',
|
|
29
|
+
options: {
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
remotes: [
|
|
32
|
+
{
|
|
33
|
+
name: 'remote1',
|
|
34
|
+
alias: 'remote1_alias',
|
|
35
|
+
entry: 'http://localhost:3001/remoteEntry.js',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
snapshotHandler: {
|
|
40
|
+
loadRemoteSnapshotInfo: jest.fn(),
|
|
41
|
+
},
|
|
42
|
+
remoteHandler: {
|
|
43
|
+
hooks: {
|
|
44
|
+
lifecycle: {
|
|
45
|
+
generatePreloadAssets: {
|
|
46
|
+
emit: jest.fn(),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should log an error if id is not provided', async () => {
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
await prefetch({ instance: mockInstance });
|
|
57
|
+
expect(logger.error).toHaveBeenCalledWith('id is required for prefetch!');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should log an error if instance is not provided', async () => {
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
await prefetch({ id: 'remote1/component1' });
|
|
63
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
64
|
+
'instance is required for prefetch!',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should log an error if remote is not found', async () => {
|
|
69
|
+
(helpers.utils.matchRemoteWithNameAndExpose as jest.Mock).mockReturnValue(
|
|
70
|
+
undefined,
|
|
71
|
+
);
|
|
72
|
+
await prefetch({ id: 'nonexistent/component', instance: mockInstance });
|
|
73
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
74
|
+
`Can not found 'nonexistent/component' in instance.options.remotes!`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should successfully prefetch data and component resources', async () => {
|
|
79
|
+
const mockRemoteInfo = {
|
|
80
|
+
remote: { name: 'remote1', alias: 'remote1_alias' },
|
|
81
|
+
expose: './component1',
|
|
82
|
+
};
|
|
83
|
+
(helpers.utils.matchRemoteWithNameAndExpose as jest.Mock).mockReturnValue(
|
|
84
|
+
mockRemoteInfo,
|
|
85
|
+
);
|
|
86
|
+
(
|
|
87
|
+
mockInstance.snapshotHandler.loadRemoteSnapshotInfo as jest.Mock
|
|
88
|
+
).mockResolvedValue({
|
|
89
|
+
remoteSnapshot: {},
|
|
90
|
+
globalSnapshot: {},
|
|
91
|
+
});
|
|
92
|
+
(helpers.utils.getRemoteInfo as jest.Mock).mockReturnValue({});
|
|
93
|
+
|
|
94
|
+
const mockDataFetchFn = jest
|
|
95
|
+
.fn()
|
|
96
|
+
.mockResolvedValue({ data: 'prefetched data' });
|
|
97
|
+
const mockGetDataFetchGetter = jest.fn().mockResolvedValue(mockDataFetchFn);
|
|
98
|
+
const mockDataFetchMap = {
|
|
99
|
+
'remote1_alias@remote1/component1': [
|
|
100
|
+
[mockGetDataFetchGetter, 'GET', undefined],
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
(utils.getDataFetchMap as jest.Mock).mockReturnValue(mockDataFetchMap);
|
|
104
|
+
(utils.getDataFetchInfo as jest.Mock).mockReturnValue({
|
|
105
|
+
name: 'remote1',
|
|
106
|
+
alias: 'remote1_alias',
|
|
107
|
+
id: 'remote1/component1',
|
|
108
|
+
});
|
|
109
|
+
(utils.getDataFetchMapKey as jest.Mock).mockReturnValue(
|
|
110
|
+
'remote1_alias@remote1/component1',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await prefetch({
|
|
114
|
+
id: 'remote1/component1',
|
|
115
|
+
instance: mockInstance,
|
|
116
|
+
dataFetchParams: { some: 'param', isDowngrade: false } as any,
|
|
117
|
+
preloadComponentResource: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
mockInstance.remoteHandler.hooks.lifecycle.generatePreloadAssets.emit,
|
|
122
|
+
).toHaveBeenCalled();
|
|
123
|
+
|
|
124
|
+
expect(mockGetDataFetchGetter).toHaveBeenCalled();
|
|
125
|
+
await new Promise(process.nextTick);
|
|
126
|
+
expect(mockDataFetchFn).toHaveBeenCalledWith({
|
|
127
|
+
some: 'param',
|
|
128
|
+
_id: 'remote1_alias@remote1/component1',
|
|
129
|
+
isDowngrade: false,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle cases where data fetch info is not available', async () => {
|
|
134
|
+
const mockRemoteInfo = {
|
|
135
|
+
remote: { name: 'remote1', alias: 'remote1_alias' },
|
|
136
|
+
expose: './component1',
|
|
137
|
+
};
|
|
138
|
+
(helpers.utils.matchRemoteWithNameAndExpose as jest.Mock).mockReturnValue(
|
|
139
|
+
mockRemoteInfo,
|
|
140
|
+
);
|
|
141
|
+
(
|
|
142
|
+
mockInstance.snapshotHandler.loadRemoteSnapshotInfo as jest.Mock
|
|
143
|
+
).mockResolvedValue({
|
|
144
|
+
remoteSnapshot: {},
|
|
145
|
+
globalSnapshot: {},
|
|
146
|
+
});
|
|
147
|
+
(utils.getDataFetchMap as jest.Mock).mockReturnValue(undefined);
|
|
148
|
+
|
|
149
|
+
await prefetch({
|
|
150
|
+
id: 'remote1/component1',
|
|
151
|
+
instance: mockInstance,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(utils.getDataFetchInfo).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
});
|