webpack-federation-types-plugin 1.0.0
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/FederationTypesPlugin.js +173 -0
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/package.json +31 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const validate = require("schema-utils")
|
|
2
|
+
const ts = require("typescript")
|
|
3
|
+
const axios = require("axios")
|
|
4
|
+
const fs = require("fs-extra")
|
|
5
|
+
const path = require("path")
|
|
6
|
+
|
|
7
|
+
const PLUGIN_NAME = "FederationTypesPlugin"
|
|
8
|
+
const MF_TYPES_DIR = "federation-types"
|
|
9
|
+
const DECLARATION_FILE_EXT = ".d.ts"
|
|
10
|
+
|
|
11
|
+
const tscOptions = {
|
|
12
|
+
allowJs: true,
|
|
13
|
+
declaration: true,
|
|
14
|
+
emitDeclarationOnly: true,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const optionsSchema = {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
ignoreRemotes: {
|
|
21
|
+
type: "array",
|
|
22
|
+
},
|
|
23
|
+
exposeTypes: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
},
|
|
26
|
+
importTypes: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getAssetData = (content) => {
|
|
33
|
+
const fileBuffer = Buffer.from(content, "utf-8")
|
|
34
|
+
const fileData = {
|
|
35
|
+
source: () => fileBuffer,
|
|
36
|
+
size: () => fileBuffer.length,
|
|
37
|
+
}
|
|
38
|
+
return fileData
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class FederationTypesPlugin {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
validate(optionsSchema, options)
|
|
44
|
+
this._options = options
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import("webpack").Compiler} compiler
|
|
49
|
+
*/
|
|
50
|
+
apply(compiler) {
|
|
51
|
+
const federationPlugin =
|
|
52
|
+
compiler.options.plugins && compiler.options.plugins.find((plugin) => plugin.constructor.name === "ModuleFederationPlugin")
|
|
53
|
+
if (!federationPlugin) throw new Error("No ModuleFederationPlugin found.")
|
|
54
|
+
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME)
|
|
55
|
+
|
|
56
|
+
const exposes = Object.fromEntries(Object.entries(federationPlugin._options.exposes || {}).map(([k, v]) => [k.replace("./", ""), v]))
|
|
57
|
+
|
|
58
|
+
const modulesPathsMap = Object.fromEntries(
|
|
59
|
+
Object.entries(exposes).map(([name, currentPath]) => {
|
|
60
|
+
const absPath = path.resolve(process.cwd(), currentPath)
|
|
61
|
+
const extension = path.extname(currentPath) ? "" : fs.existsSync(absPath + ".jsx") ? ".jsx" : ".js"
|
|
62
|
+
return [name, path.join(process.cwd(), currentPath + extension).replace(/\\/g, "/")]
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const declarationFileToExposeNameMap = Object.entries(modulesPathsMap).reduce(
|
|
67
|
+
(result, [key, val]) => Object.assign(result, {[val.replace(/\.jsx?$/, DECLARATION_FILE_EXT)]: key}),
|
|
68
|
+
{}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const modulesPaths = Object.values(modulesPathsMap)
|
|
72
|
+
|
|
73
|
+
const createTypesDefinitions = (compilation) => {
|
|
74
|
+
const host = ts.createCompilerHost(tscOptions)
|
|
75
|
+
|
|
76
|
+
const createdFiles = {}
|
|
77
|
+
host.writeFile = (fileName, fileContent) => {
|
|
78
|
+
const isExposedModules = modulesPaths.some((current) => current.startsWith(fileName.replace(/\.d\.ts/, "")))
|
|
79
|
+
if (isExposedModules) {
|
|
80
|
+
createdFiles[fileName] = fileContent
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const program = ts.createProgram(modulesPaths, tscOptions, host)
|
|
85
|
+
program.emit()
|
|
86
|
+
|
|
87
|
+
// create definitions files
|
|
88
|
+
modulesPaths.forEach((modulePath) => {
|
|
89
|
+
const typeDefFilePath = modulePath.replace(/\.jsx?$/, DECLARATION_FILE_EXT)
|
|
90
|
+
const typeDefFileContent = createdFiles[typeDefFilePath]
|
|
91
|
+
if (typeDefFileContent) {
|
|
92
|
+
compilation.emitAsset(
|
|
93
|
+
path.join(MF_TYPES_DIR, declarationFileToExposeNameMap[typeDefFilePath] + DECLARATION_FILE_EXT),
|
|
94
|
+
getAssetData(typeDefFileContent)
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
logger.warn(`failed to create ${typeDefFilePath}`)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// create index.json
|
|
102
|
+
const modulesNames = Object.keys(exposes).map((moduleName) => moduleName + DECLARATION_FILE_EXT)
|
|
103
|
+
compilation.emitAsset(path.join(MF_TYPES_DIR, "index.json"), getAssetData(JSON.stringify(modulesNames)))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getRemoteDeclareDirPath = (remoteName) => path.resolve(process.cwd(), "node_modules", "@types", remoteName)
|
|
107
|
+
|
|
108
|
+
const getTypesDefinitions = async () => {
|
|
109
|
+
const remotes = federationPlugin._options.remotes
|
|
110
|
+
if (!remotes) return
|
|
111
|
+
|
|
112
|
+
const ignoreRemotes = this._options.ignoreRemotes || []
|
|
113
|
+
const wantedRemotes = Object.entries(remotes).filter((remote) => ignoreRemotes.find((c) => c !== remote[0]))
|
|
114
|
+
|
|
115
|
+
for (const [remoteName, remoteEntryUri] of wantedRemotes) {
|
|
116
|
+
const {origin, pathname} = new URL(remoteEntryUri.split("@")[1])
|
|
117
|
+
const remotePublicUrl = origin + pathname.slice(0, pathname.lastIndexOf("/") + 1)
|
|
118
|
+
const remoteDeclareDirPath = getRemoteDeclareDirPath(remoteName)
|
|
119
|
+
const federationTypesUrl = remotePublicUrl + MF_TYPES_DIR + "/"
|
|
120
|
+
|
|
121
|
+
axios
|
|
122
|
+
// get types index from the current remote
|
|
123
|
+
.get(federationTypesUrl + "index.json")
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
if (error.response?.status === 404) logger.warn(`WARNING: remote ${remoteName} has no types`)
|
|
126
|
+
else logger.error("Failed to get remote types index", error.message)
|
|
127
|
+
})
|
|
128
|
+
.then((response) => response?.data)
|
|
129
|
+
.then((modulesNames) => {
|
|
130
|
+
if (!modulesNames) return
|
|
131
|
+
// for each remote module get his types
|
|
132
|
+
modulesNames.forEach((moduleName) => {
|
|
133
|
+
const moduleDeclarationFileUrl = federationTypesUrl + moduleName
|
|
134
|
+
axios
|
|
135
|
+
.get(moduleDeclarationFileUrl)
|
|
136
|
+
.catch((error) => {
|
|
137
|
+
logger.error(`Failed to get ${moduleDeclarationFileUrl} ${error.message}`)
|
|
138
|
+
})
|
|
139
|
+
.then((declarationFileResponse) => {
|
|
140
|
+
if (!declarationFileResponse) return
|
|
141
|
+
const declarationFileContent = declarationFileResponse.data
|
|
142
|
+
const decFilePath = path.join(remoteDeclareDirPath, moduleName)
|
|
143
|
+
fs.writeFile(decFilePath, declarationFileContent, {recursive: true})
|
|
144
|
+
fs.promises
|
|
145
|
+
.mkdir(path.dirname(decFilePath), {recursive: true})
|
|
146
|
+
.catch((error) => logger.error(`Failed to write dir: ${decFilePath}`, error))
|
|
147
|
+
.then(() => fs.writeFile(decFilePath, declarationFileContent, {recursive: true}))
|
|
148
|
+
.catch((error) => logger.error(`Failed to write declaration file: ${decFilePath}`, error))
|
|
149
|
+
})
|
|
150
|
+
.catch((error) => {
|
|
151
|
+
logger.error(`Failed to get ${moduleDeclarationFileUrl} ${error.message}`)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
.catch((error) => {
|
|
156
|
+
logger.error("Failed to add declaration file", error.message)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|
162
|
+
compilation.hooks.beforeCodeGeneration.tap(PLUGIN_NAME, () => {
|
|
163
|
+
if (this._options.exposeTypes !== false) createTypesDefinitions(compilation)
|
|
164
|
+
if (compilation.options.mode === "development" && this._options.importTypes !== false) {
|
|
165
|
+
getTypesDefinitions()
|
|
166
|
+
// TODO - call getTypesDefinitions using setInterval
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = FederationTypesPlugin
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Daniel Amenou
|
|
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,44 @@
|
|
|
1
|
+
# webpack-federation-types-plugin
|
|
2
|
+
|
|
3
|
+
This plugin adds type declaration for remote modules.
|
|
4
|
+
It compiles the exposed files into type declaration files and shares them as public files
|
|
5
|
+
On the consumer side this plugin will fetch the remote types declaration files and will add them to the "node_modules/@types" directory
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
`npm i --save-dev webpack-federation-types-plugin`
|
|
10
|
+
|
|
11
|
+
`yarn add --dev webpack-federation-types-plugin`
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const FederationTypesPlugin = require("webpack-federation-types-plugin")
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
// ...
|
|
20
|
+
plugins: [new FederationTypesPlugin({ignoreRemotes: ["remoteName"], importTypes: true, exposeTypes: true})],
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- This plugin should be added to the browser config
|
|
25
|
+
|
|
26
|
+
## Plugin Options
|
|
27
|
+
|
|
28
|
+
##### exposeTypes
|
|
29
|
+
|
|
30
|
+
Type: boolean
|
|
31
|
+
Default: true
|
|
32
|
+
Description: create and share the declaration files
|
|
33
|
+
##### importTypes
|
|
34
|
+
|
|
35
|
+
Type: boolean
|
|
36
|
+
Default: true
|
|
37
|
+
Description: fetch the remotes declaration files and add them to the @types directory
|
|
38
|
+
|
|
39
|
+
##### ignoreRemotes
|
|
40
|
+
|
|
41
|
+
Type: string[]
|
|
42
|
+
Default: undefined
|
|
43
|
+
Description: remotes to ignore
|
|
44
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webpack-federation-types-plugin",
|
|
3
|
+
"description": "This plugin adds type definitions for remote modules",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "FederationTypesPlugin.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"publish:patch": "npm version patch -m \"patch version - %s\" && npm publish",
|
|
8
|
+
"publish:minor": "npm version minor -m \"minor version - %s\" && npm publish",
|
|
9
|
+
"publish:major": "npm version major -m \"major version - %s\" && npm publish",
|
|
10
|
+
"postpublish": "git push && git push --tags"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"webpack",
|
|
14
|
+
"plugin",
|
|
15
|
+
"types",
|
|
16
|
+
"module federation"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/DanielAmenou/webpack-federation-types-plugin#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/DanielAmenou/webpack-federation-types-plugin.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "Daniel Amenou <amenou.daniel@gmail.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"axios": "^1.2.2",
|
|
27
|
+
"fs-extra": "^11.1.0",
|
|
28
|
+
"schema-utils": "^4.0.0",
|
|
29
|
+
"typescript": "^4.9.4"
|
|
30
|
+
}
|
|
31
|
+
}
|