├── .gitignore ├── LICENSE ├── README-zh_CN.md ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── core │ ├── getSharedDependencies.ts │ ├── injectExternalDepsForModule.ts │ └── loadRequireFuncOfBundle.ts ├── index.ts └── utils │ └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Eric Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |

lite-module-federation

2 | 3 |
4 | 5 | Lighter weight than webpack 5 module federation 6 | 7 | [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url] 8 | 9 | [npm-image]: http://img.shields.io/npm/v/lite-module-federation.svg?style=flat-square 10 | [npm-url]: http://npmjs.org/package/lite-module-federation 11 | [download-image]: https://img.shields.io/npm/dm/lite-module-federation.svg?style=flat-square 12 | [download-url]: https://npmjs.org/package/lite-module-federation 13 | 14 |
15 | 16 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9706d135f6034b35ad0093a38170c2c8~tplv-k3u1fbpfcp-watermark.image?) 17 | 18 | [English](./README.md) | 简体中文 19 | 20 | ## lite-module-federation 是什么? 21 | 22 | `lite-module-federation` 是一个外部模块动态加载方案,比 Webpack 5 [Module Federation](https://webpack.docschina.org/concepts/module-federation/#motivation) 更加轻量。提供 `runtime` 时,动态加载 `React` 模块的能力。 23 | 24 | ```js 25 | import { memorizedFetchBundle } from 'lite-module-federation'; 26 | 27 | const { config } = await memorizedFetchBundle( 28 | 'http://localhost:7001/cdn/remoteEntry.js' 29 | ); 30 | const PluginOne = config.componentOne; 31 | 32 | ReactDom.render(, document.getElementById('app')); 33 | ``` 34 | 35 | **Module Federation 的缺点** 36 | 37 | 1. 存在使用前提,需要子项目和父项目均升级至 Webpack 5 38 | 2. 配置繁琐,对于使用者来说不易理解,文档不够清晰,心智负担重。 39 | 3. 如果是动态加载 `remoteEntry` 模块,还需要在宿主应用增加额外的 `shared` 激活逻辑。另外为了使用 `Module Federation`,需要将宿主应用入口文件改造为异步加载,存在侵入性。 40 | 41 | 我们希望对宿主应用的侵入性尽可能小,并且轻量且好用。 42 | 43 | ## 安装 44 | 45 | ```js 46 | npm install lite-module-federation 47 | ``` 48 | 49 | ## 共享依赖 50 | 51 | 宿主项目和子项目之间,可以进行依赖共享。 52 | 53 | ### 宿主项目配置 54 | 55 | 在宿主项目根目录,我们新建 `lite-module-federation.config.js` 文件,我们在该文件中,配置需要与子应用共享的依赖。 56 | 57 | ```js 58 | // lite-module-federation.config.js 59 | module.exports = { 60 | shared: { 61 | react: require('react'), 62 | }, 63 | }; 64 | ``` 65 | 66 | 之后,我们需要在宿主应用的 Webpack 配置中添加 `lite-module-federation.config.js` 的 `alias` 67 | 68 | ```js 69 | module.exports = { 70 | resolve: { 71 | alias: { 72 | 'lite-module-federation.config.js': path.resolve( 73 | __dirname, 74 | 'lite-module-federation.config.js' 75 | ), 76 | }, 77 | }, 78 | }; 79 | ``` 80 | 81 | ### 子项目配置 82 | 83 | 对于子项目的 `webpack.config.js` 来说,需要改变如下 2 项配置 84 | 85 | - 改变输出格式为 `commonjs` 86 | - 设置外置依赖 `externals`,这样在打包时,不会将如下依赖打入 bundle 中 87 | 88 | ```js 89 | module.exports = { 90 | output: { 91 | libraryTarget: 'commonjs', 92 | }, 93 | externals: { 94 | react: 'react', 95 | }, 96 | }; 97 | ``` 98 | 99 | ### 实际应用 100 | 101 | 这是一个父子应用的场景,存在 2 个项目,一个是子应用,另一个是父应用。 102 | 103 | 下图中,我们本地启动了父应用,其中的 `plugin-1` 和 `plugin-2` 组件,是动态加载的远程子应用 `remoteEntry.js` 104 | 105 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd54d82634364e0d990d21dcf7ffcb4b~tplv-k3u1fbpfcp-watermark.image?) 106 | 107 | #### 子应用代码 108 | 109 | 在子应用中,我们 `export` 出一个 `config` 出去,这里类似于 Module Federation 的 `exposes` 属性,可以导出多个组件。这里在打包时的区别是,我们会在 `webpack` 中`externals` 配置 `react`。也就是不将 `React` 进行打包。 110 | 111 | ```js 112 | // ./src/plugin 113 | import React from 'react'; 114 | import './test.css'; 115 | 116 | const PluginOne: React.FC<{}> = () => { 117 | return
plugin-1
; 118 | }; 119 | const PluginTwo: React.FC<{}> = () => { 120 | return
plugin-2
; 121 | }; 122 | 123 | export const config = { 124 | componentOne: PluginOne, 125 | componentTwo: PluginTwo, 126 | }; 127 | ``` 128 | 129 | 子应用侧 `Webpack` 配置如下: 130 | 131 | ```js 132 | const webpack = require('webpack'); 133 | module.exports = { 134 | mode: 'development', 135 | entry: { 136 | remoteEntry: './src/plugin.tsx', 137 | }, 138 | output: { 139 | filename: '[name].js', 140 | libraryTarget: 'commonjs', 141 | }, 142 | plugins: [ 143 | new webpack.optimize.LimitChunkCountPlugin({ 144 | maxChunks: 1, 145 | }), 146 | ], 147 | externals: { 148 | react: 'react', 149 | }, 150 | devServer: { 151 | hot: true, 152 | port: 9001, 153 | headers: { 154 | 'Access-Control-Allow-Origin': '*', 155 | }, 156 | }, 157 | }; 158 | ``` 159 | 160 | 接下来,我们将子应用打包 161 | 162 | ```js 163 | npm run build 164 | ``` 165 | 166 | 打包后得到 `remoteEntry.js`,我们将其上传至 `CDN` 上。 167 | 168 | #### 宿主侧使用插件 169 | 170 | 宿主侧,我们使用 `lite-module-federation` 包,解析刚刚发布到 CDN 中打包好的 `remoteEntry.js`。 171 | 在执行完 `memorizedFetchBundle` 方法后,我们可以加载出注入依赖后的 `remoteEntry.js`,拿到其中的 `config` 属性,将组件渲染到页面中。 172 | 173 | ```js 174 | import ReactDom from 'react-dom'; 175 | import React, { useEffect, useState } from 'react'; 176 | import { memorizedFetchBundle } from 'lite-module-federation'; 177 | import './app.css'; 178 | 179 | const App: React.FC<{}> = () => { 180 | const [config, setConfig] = useState>({}); 181 | const [loading, setLoading] = useState(true); 182 | 183 | useEffect(() => { 184 | setTimeout(async () => { 185 | const { config } = await memorizedFetchBundle( 186 | 'http://localhost:7001/cdn/remoteEntry.js' 187 | ); 188 | setConfig(config); 189 | 190 | setLoading(false); 191 | }, 1000); 192 | }, []); 193 | 194 | if (loading) { 195 | return
Loading sub-app.....
; 196 | } 197 | 198 | const PluginOne = config.componentOne; 199 | const PluginTwo = config.componentTwo; 200 | 201 | return ( 202 |
203 |
Main App
204 |
205 | 206 | 207 |
208 |
209 | ); 210 | }; 211 | 212 | ReactDom.render(, document.getElementById('app')); 213 | ``` 214 | 215 | ### 使用场景 216 | 217 | Module Federation 的使用场景: 218 | 219 | - 需要运行在宿主应用的沙箱环境中,子应用希望与父应用共享同一个上下文 220 | 221 | - 父子应用不需要样式环境隔离 222 | 223 | `lite-module-federation` 的使用场景也与 Module Federation 相同,当你有遇到如上场景时,又需要一个更轻量的方案时,可以使用`lite-module-federation`。 224 | 225 | ### 最佳实践 226 | 227 | https://juejin.cn/post/7170613119755452446 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

lite-module-federation

2 | 3 |
4 | 5 | Lighter weight than webpack 5 module federation 6 | 7 | [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url] 8 | 9 | [npm-image]: http://img.shields.io/npm/v/lite-module-federation.svg?style=flat-square 10 | [npm-url]: http://npmjs.org/package/lite-module-federation 11 | [download-image]: https://img.shields.io/npm/dm/lite-module-federation.svg?style=flat-square 12 | [download-url]: https://npmjs.org/package/lite-module-federation 13 | 14 |
15 | 16 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9706d135f6034b35ad0093a38170c2c8~tplv-k3u1fbpfcp-watermark.image?) 17 | 18 | English | [简体中文](./README-zh_CN.md) 19 | 20 | ## What is lite-module-federation? 21 | 22 | `lite-module-federation` is an external module dynamic loading scheme, which is more lightweight than Webpack 5 [Module Federation](https://webpack.docschina.org/concepts/module-federation/#motivation). Provides the ability to dynamically load `React` modules when `runtime` is provided. 23 | 24 | ```js 25 | import { memorizedFetchBundle } from 'lite-module-federation'; 26 | 27 | const { config } = await memorizedFetchBundle( 28 | 'http://localhost:7001/cdn/remoteEntry.js' 29 | ); 30 | const PluginOne = config.componentOne; 31 | 32 | ReactDom.render(, document.getElementById('app')); 33 | ``` 34 | 35 | **Disadvantages of Module Federation** 36 | 37 | 1. There are prerequisites for use, and both sub-projects and parent projects need to be upgraded to Webpack 5 38 | 2. The configuration is cumbersome, it is not easy for users to understand, the documentation is not clear enough, and the mental burden is heavy. 39 | 3. If the `remoteEntry` module is loaded dynamically, additional `shared` activation logic needs to be added to the host application. In addition, in order to use `Module Federation`, the host application entry file needs to be transformed into asynchronous loading, which is intrusive. 40 | 41 | We want to be as intrusive as possible to the host application, and lightweight and easy to use. 42 | 43 | ## Install 44 | 45 | ```js 46 | npm install lite-module-federation 47 | ``` 48 | 49 | ## Shared dependencies 50 | 51 | Dependencies can be shared between the host project and subprojects. 52 | 53 | ### Host project configuration 54 | 55 | In the root directory of the host project, we create a new `lite-module-federation.config.js` file, in which we configure the dependencies that need to be shared with sub-applications. 56 | 57 | ```js 58 | // lite-module-federation.config.js 59 | module.exports = { 60 | shared: { 61 | react: require('react'), 62 | }, 63 | }; 64 | ``` 65 | 66 | After that, we need to add the `alias` of `lite-module-federation.config.js` to the webpack configuration of the host application 67 | 68 | ```js 69 | module.exports = { 70 | resolve: { 71 | alias: { 72 | 'lite-module-federation.config.js': path.resolve( 73 | __dirname, 74 | 'lite-module-federation.config.js' 75 | ), 76 | }, 77 | }, 78 | }; 79 | ``` 80 | 81 | ### Subproject configuration 82 | 83 | For the `webpack.config.js` of the sub-project, you need to change the following 2 configurations 84 | 85 | - Change output format to `commonjs` 86 | - Set external dependencies `externals`, so that the following dependencies will not be included in the bundle when packaging 87 | 88 | ```js 89 | module.exports = { 90 | output: { 91 | libraryTarget: 'commonjs', 92 | }, 93 | externals: { 94 | react: 'react', 95 | }, 96 | }; 97 | ``` 98 | 99 | ### Practical application 100 | 101 | This is a parent-child application scenario, there are 2 projects, one is the child application and the other is the parent application. 102 | 103 | In the figure below, we start the parent application locally, and the `plugin-1` and `plugin-2` components are dynamically loaded remote sub-applications `remoteEntry.js` 104 | 105 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd54d82634364e0d990d21dcf7ffcb4b~tplv-k3u1fbpfcp-watermark.image?) 106 | 107 | #### Sub app code 108 | 109 | In the sub-application, we `export` out a `config`, which is similar to the `exposes` property of Module Federation, which can export multiple components. The difference here when bundling is that we will configure `react` in `externals` in `webpack`. That is, do not package `React`. 110 | 111 | ```js 112 | // ./src/plugin 113 | import React from 'react'; 114 | import './test.css'; 115 | 116 | const PluginOne: React.FC<{}> = () => { 117 | return
plugin-1
; 118 | }; 119 | const PluginTwo: React.FC<{}> = () => { 120 | return
plugin-2
; 121 | }; 122 | 123 | export const config = { 124 | componentOne: PluginOne, 125 | componentTwo: PluginTwo, 126 | }; 127 | ``` 128 | 129 | The `Webpack` configuration on the sub-application side is as follows: 130 | 131 | ```js 132 | const webpack = require('webpack'); 133 | module.exports = { 134 | mode: 'development', 135 | entry: { 136 | remoteEntry: './src/plugin.tsx', 137 | }, 138 | output: { 139 | filename: '[name].js', 140 | libraryTarget: 'commonjs', 141 | }, 142 | plugins: [ 143 | new webpack.optimize.LimitChunkCountPlugin({ 144 | maxChunks: 1, 145 | }), 146 | ], 147 | externals: { 148 | react: 'react', 149 | }, 150 | devServer: { 151 | hot: true, 152 | port: 9001, 153 | headers: { 154 | 'Access-Control-Allow-Origin': '*', 155 | }, 156 | }, 157 | }; 158 | ``` 159 | 160 | Next, we package the sub-app 161 | 162 | ```js 163 | npm run build 164 | ``` 165 | 166 | After packaging, we get `remoteEntry.js`, and we upload it to `CDN`. 167 | 168 | #### Using plugins on the host side 169 | 170 | On the host side, we use the `lite-module-federation` package to parse the packaged `remoteEntry.js` just published to the CDN. 171 | After executing the `memorizedFetchBundle` method, we can load the injected dependency `remoteEntry.js`, get the `config` property, and render the component to the page. 172 | 173 | ```js 174 | import ReactDom from 'react-dom'; 175 | import React, { useEffect, useState } from 'react'; 176 | import { memorizedFetchBundle } from 'lite-module-federation'; 177 | import './app.css'; 178 | 179 | const App: React.FC<{}> = () => { 180 | const [config, setConfig] = useState>({}); 181 | const [loading, setLoading] = useState(true); 182 | 183 | useEffect(() => { 184 | setTimeout(async() => { 185 | const { config } = await memorizedFetchBundle( 186 | 'http://localhost:7001/cdn/remoteEntry.js' 187 | ); 188 | setConfig(config); 189 | 190 | setLoading(false); 191 | }, 1000); 192 | }, []); 193 | 194 | if (loading) { 195 | return
Loading sub-app.....
; 196 | } 197 | 198 | const PluginOne = config.componentOne; 199 | const PluginTwo = config.componentTwo; 200 | 201 | return ( 202 |
203 |
Main App
204 |
205 | 206 | 207 |
208 |
209 | ); 210 | }; 211 | 212 | ReactDom.render(, document.getElementById('app')); 213 | ``` 214 | 215 | ### Scenes to be used 216 | 217 | Module Federation usage scenarios: 218 | 219 | - It needs to run in the sandbox environment of the host application, and the child application wants to share the same context as the parent application 220 | 221 | - Parent-child applications do not require style environment isolation 222 | 223 | The usage scenario of `lite-module-federation` is also the same as Module Federation. When you encounter the above scenario and need a lighter solution, you can use `lite-module-federation`. 224 | 225 | ### Best Practices 226 | 227 | https://github.com/ericlee33/remote-plugin-dev 228 | 229 | https://juejin.cn/post/7170613119755452446 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lite-module-federation", 3 | "version": "1.0.4", 4 | "description": "Lighter weight than webpack module federation", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "rm -rf ./dist && tsc", 8 | "prepublishOnly": "pnpm build" 9 | }, 10 | "keywords": [ 11 | "module-federation", 12 | "webpack", 13 | "loader", 14 | "micro-frontend" 15 | ], 16 | "author": "Eric lee", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@types/node": "^18.11.9", 20 | "typescript": "^4.9.3" 21 | }, 22 | "peerDependencies": { 23 | "react": ">=16.0", 24 | "react-dom": ">=16.0" 25 | }, 26 | "files": [ 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | '@types/node': ^18.11.9 5 | axios: ^1.2.0 6 | typescript: ^4.9.3 7 | 8 | dependencies: 9 | axios: 1.2.0 10 | 11 | devDependencies: 12 | '@types/node': 18.11.9 13 | typescript: 4.9.3 14 | 15 | packages: 16 | 17 | /@types/node/18.11.9: 18 | resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} 19 | dev: true 20 | 21 | /asynckit/0.4.0: 22 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 23 | dev: false 24 | 25 | /axios/1.2.0: 26 | resolution: {integrity: sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==} 27 | dependencies: 28 | follow-redirects: 1.15.2 29 | form-data: 4.0.0 30 | proxy-from-env: 1.1.0 31 | transitivePeerDependencies: 32 | - debug 33 | dev: false 34 | 35 | /combined-stream/1.0.8: 36 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 37 | engines: {node: '>= 0.8'} 38 | dependencies: 39 | delayed-stream: 1.0.0 40 | dev: false 41 | 42 | /delayed-stream/1.0.0: 43 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 44 | engines: {node: '>=0.4.0'} 45 | dev: false 46 | 47 | /follow-redirects/1.15.2: 48 | resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} 49 | engines: {node: '>=4.0'} 50 | peerDependencies: 51 | debug: '*' 52 | peerDependenciesMeta: 53 | debug: 54 | optional: true 55 | dev: false 56 | 57 | /form-data/4.0.0: 58 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 59 | engines: {node: '>= 6'} 60 | dependencies: 61 | asynckit: 0.4.0 62 | combined-stream: 1.0.8 63 | mime-types: 2.1.35 64 | dev: false 65 | 66 | /mime-db/1.52.0: 67 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 68 | engines: {node: '>= 0.6'} 69 | dev: false 70 | 71 | /mime-types/2.1.35: 72 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 73 | engines: {node: '>= 0.6'} 74 | dependencies: 75 | mime-db: 1.52.0 76 | dev: false 77 | 78 | /proxy-from-env/1.1.0: 79 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 80 | dev: false 81 | 82 | /typescript/4.9.3: 83 | resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} 84 | engines: {node: '>=4.2.0'} 85 | hasBin: true 86 | dev: true 87 | -------------------------------------------------------------------------------- /src/core/getSharedDependencies.ts: -------------------------------------------------------------------------------- 1 | type GetSharedDependencies = () => { 2 | [key: string]: string; 3 | }; 4 | 5 | export const getSharedDependencies: GetSharedDependencies = () => { 6 | const config = require('lite-module-federation.config.js'); 7 | 8 | if (!config.shared) { 9 | throw new Error( 10 | `cannot found registered \"shared\" deps in 'lite-module-federation.config.js' ` 11 | ); 12 | } 13 | 14 | return config.shared; 15 | }; 16 | -------------------------------------------------------------------------------- /src/core/injectExternalDepsForModule.ts: -------------------------------------------------------------------------------- 1 | import { Require } from './loadRequireFuncOfBundle'; 2 | 3 | type InjectExternalDepsForModule = ( 4 | require: Require, 5 | bundle: string 6 | ) => Record; 7 | 8 | const evalBundleWithNewFunction = (bundle) => 9 | new Function('require', 'module', 'exports', bundle); 10 | 11 | export const injectExternalDepsForModule: InjectExternalDepsForModule = ( 12 | require, 13 | bundle 14 | ) => { 15 | const exports = {}; 16 | const module = { exports }; 17 | // if we configure the webpack external option with `react` 18 | // the bundle will have `require('react')` methods, so we need to inject this method 19 | 20 | // note: bundle must be formatted as commonjs 21 | evalBundleWithNewFunction(bundle)(require, module, exports); 22 | return module.exports; 23 | }; 24 | -------------------------------------------------------------------------------- /src/core/loadRequireFuncOfBundle.ts: -------------------------------------------------------------------------------- 1 | import { getSharedDependencies } from './getSharedDependencies'; 2 | 3 | export type Require = (name: string) => string; 4 | 5 | type LoadRequireFuncOfBundle = () => Require; 6 | 7 | export const loadRequireFuncOfBundle: LoadRequireFuncOfBundle = () => { 8 | const sharedDeps = getSharedDependencies() || {}; 9 | 10 | return (packageName) => { 11 | if (!(packageName in sharedDeps)) { 12 | throw new Error( 13 | `cannot require '${packageName}'. because '${packageName}' does not exist in shared dependencies.` 14 | ); 15 | } 16 | 17 | return sharedDeps[packageName]; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from './utils'; 2 | import { injectExternalDepsForModule } from './core/injectExternalDepsForModule'; 3 | import { loadRequireFuncOfBundle } from './core/loadRequireFuncOfBundle'; 4 | 5 | type MemorizedModuleFetcher = (url: string) => Promise; 6 | 7 | const showInfoWhenBundleNotFound = () => { 8 | console.warn( 9 | `[lite-module-federation] Bundle is empty, please check to see if the 'url' is correct` 10 | ); 11 | }; 12 | 13 | const fetchModule = (url, require) => { 14 | return fetch(url) 15 | .then((data) => data.text()) 16 | .then((text) => { 17 | if (!text) { 18 | showInfoWhenBundleNotFound(); 19 | } 20 | 21 | return injectExternalDepsForModule(require, text); 22 | }); 23 | }; 24 | 25 | const requireFunc = loadRequireFuncOfBundle(); 26 | 27 | /** 28 | * Fetch the bundle with cache 29 | * 30 | * e.g. memorizedFetchBundle(bundle_CDN_url: string) 31 | * 32 | * cache by `url` 33 | */ 34 | export const memorizedFetchBundle: MemorizedModuleFetcher = memoize((url) => 35 | fetchModule(url, requireFunc) 36 | ); 37 | 38 | export default memorizedFetchBundle; 39 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const memoize = (func) => { 2 | const cache = {}; 3 | return (key: string) => { 4 | if (!(key in cache)) { 5 | cache[key] = func(key); 6 | } 7 | return cache[key]; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 46 | // "declarationMap": true /* Create sourcemaps for d.ts files. */, 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true /* Create source map files for emitted JavaScript files. */, 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "exclude": ["node_modules"] 102 | } 103 | --------------------------------------------------------------------------------