@bleedingdev/modern-js-ultramodern-app-tools 3.9.0-ultramodern.16 → 3.9.0-ultramodern.18
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/dist/cjs/native-composition/module-federation-shared-plugin.js +236 -0
- package/dist/cjs/native-composition/preset.js +4 -0
- package/dist/esm/native-composition/module-federation-shared-plugin.mjs +176 -0
- package/dist/esm/native-composition/preset.mjs +4 -0
- package/dist/esm-node/native-composition/module-federation-shared-plugin.mjs +177 -0
- package/dist/esm-node/native-composition/preset.mjs +4 -0
- package/dist/types/native-composition/module-federation-shared-plugin.d.ts +45 -0
- package/dist/types-esm/native-composition/module-federation-shared-plugin.d.mts +45 -0
- package/package.json +18 -18
- package/src/native-composition/module-federation-shared-plugin.ts +367 -0
- package/src/native-composition/preset.ts +4 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
14
|
+
var define = (defs, kind)=>{
|
|
15
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
[kind]: defs[key]
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
define(getters, "get");
|
|
21
|
+
define(values, "value");
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
(()=>{
|
|
25
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
26
|
+
})();
|
|
27
|
+
(()=>{
|
|
28
|
+
__webpack_require__.r = (exports1)=>{
|
|
29
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
30
|
+
value: 'Module'
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
33
|
+
value: true
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
var __webpack_exports__ = {};
|
|
38
|
+
__webpack_require__.r(__webpack_exports__);
|
|
39
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
40
|
+
FederationPrivateContextsPlugin: ()=>FederationPrivateContextsPlugin,
|
|
41
|
+
resolveFrameworkSharedPackages: ()=>resolveFrameworkSharedPackages,
|
|
42
|
+
ultramodernModuleFederationSharedPlugin: ()=>ultramodernModuleFederationSharedPlugin,
|
|
43
|
+
withFrameworkShared: ()=>withFrameworkShared,
|
|
44
|
+
withReactJsxRuntimeShared: ()=>withReactJsxRuntimeShared
|
|
45
|
+
});
|
|
46
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
47
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
48
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
49
|
+
const REACT_JSX_RUNTIMES = [
|
|
50
|
+
'react/jsx-runtime',
|
|
51
|
+
'react/jsx-dev-runtime'
|
|
52
|
+
];
|
|
53
|
+
const MODULE_FEDERATION_CHAIN_IDS = [
|
|
54
|
+
'plugin-module-federation',
|
|
55
|
+
'plugin-module-federation-server'
|
|
56
|
+
];
|
|
57
|
+
const FRAMEWORK_SHARES = [
|
|
58
|
+
{
|
|
59
|
+
packageName: '@modern-js/runtime',
|
|
60
|
+
prefix: '@modern-js/runtime/',
|
|
61
|
+
contextsRequest: '@modern-js/runtime/context',
|
|
62
|
+
contexts: /[\\/]core[\\/]context[\\/](?:index|runtime)\.[cm]?[jt]sx?$/
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
packageName: '@modern-js/plugin-i18n',
|
|
66
|
+
prefix: '@modern-js/plugin-i18n/runtime/',
|
|
67
|
+
contextsRequest: '@modern-js/plugin-i18n/runtime/contexts',
|
|
68
|
+
contexts: /[\\/]runtime[\\/]contexts\.[cm]?[jt]sx?$/
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
packageName: '@modern-js/bff-effect',
|
|
72
|
+
prefix: '@modern-js/bff-effect/',
|
|
73
|
+
contextsRequest: '@modern-js/bff-effect/context',
|
|
74
|
+
contexts: /[\\/]effect[\\/]context\.[cm]?[jt]sx?$/
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
const loadsCode = (target)=>'string' == typeof target ? !target.endsWith('.d.ts') : Boolean(target) && 'object' == typeof target && Object.entries(target).some(([condition, value])=>'types' !== condition && loadsCode(value));
|
|
78
|
+
const exportedRequests = (packageName, prefix, exports1)=>Object.entries(exports1 && 'object' == typeof exports1 ? exports1 : {}).filter(([subpath, target])=>'./package.json' !== subpath && !subpath.includes('*') && loadsCode(target)).map(([subpath])=>`${packageName}${subpath.slice(1)}`).filter((request)=>request.startsWith(prefix)).sort();
|
|
79
|
+
const findInstalledManifest = (appDirectory, packageName)=>{
|
|
80
|
+
for(let directory = appDirectory;;){
|
|
81
|
+
const manifest = external_node_path_default().join(directory, 'node_modules', packageName, 'package.json');
|
|
82
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(manifest)) return manifest;
|
|
83
|
+
const parent = external_node_path_default().dirname(directory);
|
|
84
|
+
if (parent === directory) return;
|
|
85
|
+
directory = parent;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const resolveFrameworkSharedPackages = (appDirectory)=>FRAMEWORK_SHARES.flatMap((share)=>{
|
|
89
|
+
const manifest = findInstalledManifest(appDirectory, share.packageName);
|
|
90
|
+
if (!manifest) return [];
|
|
91
|
+
const { version, exports: exports1 } = JSON.parse((0, external_node_fs_namespaceObject.readFileSync)(manifest, 'utf8'));
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
...share,
|
|
95
|
+
version,
|
|
96
|
+
requests: exportedRequests(share.packageName, share.prefix, exports1),
|
|
97
|
+
directory: (0, external_node_fs_namespaceObject.realpathSync)(external_node_path_default().dirname(manifest))
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
});
|
|
101
|
+
const findShared = (entries, request)=>{
|
|
102
|
+
for (const entry of entries){
|
|
103
|
+
if (entry === request) return {};
|
|
104
|
+
if ('object' == typeof entry && Object.hasOwn(entry, request)) {
|
|
105
|
+
const config = entry[request];
|
|
106
|
+
return 'string' == typeof config ? {
|
|
107
|
+
requiredVersion: config
|
|
108
|
+
} : config;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const toEntries = (shared)=>Array.isArray(shared) ? shared : [
|
|
113
|
+
shared
|
|
114
|
+
];
|
|
115
|
+
const withShared = (shared, missing)=>{
|
|
116
|
+
const entries = toEntries(shared);
|
|
117
|
+
const added = Object.fromEntries(Object.entries(missing).filter(([request])=>!findShared(entries, request)));
|
|
118
|
+
if (0 === Object.keys(added).length) return shared;
|
|
119
|
+
if (!shared) return added;
|
|
120
|
+
return Array.isArray(shared) ? [
|
|
121
|
+
...shared,
|
|
122
|
+
added
|
|
123
|
+
] : {
|
|
124
|
+
...shared,
|
|
125
|
+
...added
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const withReactJsxRuntimeShared = (shared)=>{
|
|
129
|
+
if (!shared || 'object' != typeof shared) return shared;
|
|
130
|
+
const react = findShared(toEntries(shared), 'react');
|
|
131
|
+
if (!react) return shared;
|
|
132
|
+
return withShared(shared, Object.fromEntries(REACT_JSX_RUNTIMES.map((request)=>[
|
|
133
|
+
request,
|
|
134
|
+
{
|
|
135
|
+
...void 0 === react.requiredVersion ? {} : {
|
|
136
|
+
requiredVersion: react.requiredVersion
|
|
137
|
+
},
|
|
138
|
+
singleton: true,
|
|
139
|
+
treeShaking: false
|
|
140
|
+
}
|
|
141
|
+
])));
|
|
142
|
+
};
|
|
143
|
+
const withFrameworkShared = (shared, packages)=>withShared(shared, Object.fromEntries(packages.flatMap(({ requests, version })=>requests.map((request)=>[
|
|
144
|
+
request,
|
|
145
|
+
{
|
|
146
|
+
requiredVersion: version,
|
|
147
|
+
singleton: true,
|
|
148
|
+
treeShaking: false
|
|
149
|
+
}
|
|
150
|
+
]))));
|
|
151
|
+
const federationConfig = (options)=>options.mfConfig && 'object' == typeof options.mfConfig ? federationConfig(options.mfConfig) : options;
|
|
152
|
+
const withDefaults = (options, packages)=>options.mfConfig && 'object' == typeof options.mfConfig ? {
|
|
153
|
+
...options,
|
|
154
|
+
mfConfig: withDefaults(options.mfConfig, packages)
|
|
155
|
+
} : {
|
|
156
|
+
...options,
|
|
157
|
+
shared: withFrameworkShared(withReactJsxRuntimeShared(options.shared), packages)
|
|
158
|
+
};
|
|
159
|
+
const hasExposes = (options)=>{
|
|
160
|
+
const { exposes } = federationConfig(options);
|
|
161
|
+
return Array.isArray(exposes) ? exposes.length > 0 : Boolean(exposes) && Object.keys(exposes).length > 0;
|
|
162
|
+
};
|
|
163
|
+
const SHARED_MODULE_TYPES = new Set([
|
|
164
|
+
'consume-shared-module',
|
|
165
|
+
'provide-module'
|
|
166
|
+
]);
|
|
167
|
+
const resourceOf = (module)=>module && 'resource' in module && 'string' == typeof module.resource ? module.resource : void 0;
|
|
168
|
+
const describeModule = (module)=>module ? resourceOf(module) ?? module.identifier() : 'an entry';
|
|
169
|
+
const CONTAINER_ENTRY_DEPENDENCY = 'container entry';
|
|
170
|
+
const buildsContainer = (compilation)=>[
|
|
171
|
+
...compilation.entries.values()
|
|
172
|
+
].some(({ dependencies })=>dependencies.some(({ type })=>type === CONTAINER_ENTRY_DEPENDENCY));
|
|
173
|
+
class FederationPrivateContextsPlugin {
|
|
174
|
+
apply(compiler) {
|
|
175
|
+
const name = 'FederationPrivateContextsPlugin';
|
|
176
|
+
const definitionOf = (module)=>{
|
|
177
|
+
const resource = resourceOf(module);
|
|
178
|
+
return void 0 === resource ? void 0 : this.packages.find(({ contexts, directory })=>contexts?.test(resource) && resource.startsWith(directory + external_node_path_default().sep));
|
|
179
|
+
};
|
|
180
|
+
compiler.hooks.compilation.tap(name, (compilation)=>{
|
|
181
|
+
compilation.hooks.finishModules.tap(name, (modules)=>{
|
|
182
|
+
if (!buildsContainer(compilation)) return;
|
|
183
|
+
for (const module of modules){
|
|
184
|
+
const share = definitionOf(module);
|
|
185
|
+
if (!share) continue;
|
|
186
|
+
const importer = compilation.moduleGraph.getIncomingConnections(module).map((connection)=>connection.originModule).find((origin)=>!(origin && SHARED_MODULE_TYPES.has(origin.type)) && definitionOf(origin) !== share);
|
|
187
|
+
if (void 0 !== importer) compilation.errors.push(new compiler.webpack.WebpackError(`[ultramodern] This Module Federation build bundles a private copy of ${share.contextsRequest}: ${describeModule(importer)} imports ${describeModule(module)} directly. It would not see the host's request state. Share the "${share.prefix}" subpaths and import the contexts through "${share.contextsRequest}".`));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
constructor(packages){
|
|
193
|
+
this.packages = packages;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const ultramodernModuleFederationSharedPlugin = ()=>({
|
|
197
|
+
name: '@modern-js/ultramodern-module-federation-shared',
|
|
198
|
+
pre: [
|
|
199
|
+
'@modern-js/plugin-module-federation',
|
|
200
|
+
'@modern-js/plugin-module-federation-ssr'
|
|
201
|
+
],
|
|
202
|
+
setup (api) {
|
|
203
|
+
api.modifyBundlerChain((chain)=>{
|
|
204
|
+
const ids = MODULE_FEDERATION_CHAIN_IDS.filter((id)=>chain.plugins.has(id));
|
|
205
|
+
if (0 === ids.length) return;
|
|
206
|
+
const packages = resolveFrameworkSharedPackages(api.getAppContext().appDirectory);
|
|
207
|
+
let exposes = false;
|
|
208
|
+
for (const id of ids){
|
|
209
|
+
const [options] = chain.plugin(id).get('args');
|
|
210
|
+
exposes ||= hasExposes(options);
|
|
211
|
+
chain.plugin(id).tap(([options, ...rest])=>[
|
|
212
|
+
withDefaults(options, packages),
|
|
213
|
+
...rest
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
if (exposes) chain.plugin('ultramodern-federation-private-contexts').use(FederationPrivateContextsPlugin, [
|
|
217
|
+
packages
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
exports.FederationPrivateContextsPlugin = __webpack_exports__.FederationPrivateContextsPlugin;
|
|
223
|
+
exports.resolveFrameworkSharedPackages = __webpack_exports__.resolveFrameworkSharedPackages;
|
|
224
|
+
exports.ultramodernModuleFederationSharedPlugin = __webpack_exports__.ultramodernModuleFederationSharedPlugin;
|
|
225
|
+
exports.withFrameworkShared = __webpack_exports__.withFrameworkShared;
|
|
226
|
+
exports.withReactJsxRuntimeShared = __webpack_exports__.withReactJsxRuntimeShared;
|
|
227
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
228
|
+
"FederationPrivateContextsPlugin",
|
|
229
|
+
"resolveFrameworkSharedPackages",
|
|
230
|
+
"ultramodernModuleFederationSharedPlugin",
|
|
231
|
+
"withFrameworkShared",
|
|
232
|
+
"withReactJsxRuntimeShared"
|
|
233
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
234
|
+
Object.defineProperty(exports, '__esModule', {
|
|
235
|
+
value: true
|
|
236
|
+
});
|
|
@@ -51,6 +51,7 @@ const release_identity_namespaceObject = require("@modern-js/app-tools-extension
|
|
|
51
51
|
const runtime_package_resolution_namespaceObject = require("@modern-js/app-tools-extensions/runtime-package-resolution");
|
|
52
52
|
const cli_namespaceObject = require("@modern-js/plugin/cli");
|
|
53
53
|
const core_namespaceObject = require("@rsbuild/core");
|
|
54
|
+
const external_module_federation_shared_plugin_js_namespaceObject = require("./module-federation-shared-plugin.js");
|
|
54
55
|
const DEFAULT_OTLP_ENDPOINT = 'http://127.0.0.1:4318/v1/logs';
|
|
55
56
|
const DEFAULT_VICTORIA_METRICS_ENDPOINT = 'http://127.0.0.1:8428/api/v1/import/prometheus';
|
|
56
57
|
const resolveReactRouterPackageDir = (appDirectory)=>{
|
|
@@ -111,6 +112,9 @@ const createPresetUltramodernConfig = (options = {})=>{
|
|
|
111
112
|
output: {
|
|
112
113
|
precompress: true
|
|
113
114
|
},
|
|
115
|
+
plugins: [
|
|
116
|
+
(0, external_module_federation_shared_plugin_js_namespaceObject.ultramodernModuleFederationSharedPlugin)()
|
|
117
|
+
],
|
|
114
118
|
server,
|
|
115
119
|
source: {
|
|
116
120
|
reactCompiler: true,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
const REACT_JSX_RUNTIMES = [
|
|
4
|
+
'react/jsx-runtime',
|
|
5
|
+
'react/jsx-dev-runtime'
|
|
6
|
+
];
|
|
7
|
+
const MODULE_FEDERATION_CHAIN_IDS = [
|
|
8
|
+
'plugin-module-federation',
|
|
9
|
+
'plugin-module-federation-server'
|
|
10
|
+
];
|
|
11
|
+
const FRAMEWORK_SHARES = [
|
|
12
|
+
{
|
|
13
|
+
packageName: '@modern-js/runtime',
|
|
14
|
+
prefix: '@modern-js/runtime/',
|
|
15
|
+
contextsRequest: '@modern-js/runtime/context',
|
|
16
|
+
contexts: /[\\/]core[\\/]context[\\/](?:index|runtime)\.[cm]?[jt]sx?$/
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
packageName: '@modern-js/plugin-i18n',
|
|
20
|
+
prefix: '@modern-js/plugin-i18n/runtime/',
|
|
21
|
+
contextsRequest: '@modern-js/plugin-i18n/runtime/contexts',
|
|
22
|
+
contexts: /[\\/]runtime[\\/]contexts\.[cm]?[jt]sx?$/
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
packageName: '@modern-js/bff-effect',
|
|
26
|
+
prefix: '@modern-js/bff-effect/',
|
|
27
|
+
contextsRequest: '@modern-js/bff-effect/context',
|
|
28
|
+
contexts: /[\\/]effect[\\/]context\.[cm]?[jt]sx?$/
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
const loadsCode = (target)=>'string' == typeof target ? !target.endsWith('.d.ts') : Boolean(target) && 'object' == typeof target && Object.entries(target).some(([condition, value])=>'types' !== condition && loadsCode(value));
|
|
32
|
+
const exportedRequests = (packageName, prefix, exports)=>Object.entries(exports && 'object' == typeof exports ? exports : {}).filter(([subpath, target])=>'./package.json' !== subpath && !subpath.includes('*') && loadsCode(target)).map(([subpath])=>`${packageName}${subpath.slice(1)}`).filter((request)=>request.startsWith(prefix)).sort();
|
|
33
|
+
const findInstalledManifest = (appDirectory, packageName)=>{
|
|
34
|
+
for(let directory = appDirectory;;){
|
|
35
|
+
const manifest = node_path.join(directory, 'node_modules', packageName, 'package.json');
|
|
36
|
+
if (existsSync(manifest)) return manifest;
|
|
37
|
+
const parent = node_path.dirname(directory);
|
|
38
|
+
if (parent === directory) return;
|
|
39
|
+
directory = parent;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const resolveFrameworkSharedPackages = (appDirectory)=>FRAMEWORK_SHARES.flatMap((share)=>{
|
|
43
|
+
const manifest = findInstalledManifest(appDirectory, share.packageName);
|
|
44
|
+
if (!manifest) return [];
|
|
45
|
+
const { version, exports } = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
...share,
|
|
49
|
+
version,
|
|
50
|
+
requests: exportedRequests(share.packageName, share.prefix, exports),
|
|
51
|
+
directory: realpathSync(node_path.dirname(manifest))
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
});
|
|
55
|
+
const findShared = (entries, request)=>{
|
|
56
|
+
for (const entry of entries){
|
|
57
|
+
if (entry === request) return {};
|
|
58
|
+
if ('object' == typeof entry && Object.hasOwn(entry, request)) {
|
|
59
|
+
const config = entry[request];
|
|
60
|
+
return 'string' == typeof config ? {
|
|
61
|
+
requiredVersion: config
|
|
62
|
+
} : config;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const toEntries = (shared)=>Array.isArray(shared) ? shared : [
|
|
67
|
+
shared
|
|
68
|
+
];
|
|
69
|
+
const withShared = (shared, missing)=>{
|
|
70
|
+
const entries = toEntries(shared);
|
|
71
|
+
const added = Object.fromEntries(Object.entries(missing).filter(([request])=>!findShared(entries, request)));
|
|
72
|
+
if (0 === Object.keys(added).length) return shared;
|
|
73
|
+
if (!shared) return added;
|
|
74
|
+
return Array.isArray(shared) ? [
|
|
75
|
+
...shared,
|
|
76
|
+
added
|
|
77
|
+
] : {
|
|
78
|
+
...shared,
|
|
79
|
+
...added
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const withReactJsxRuntimeShared = (shared)=>{
|
|
83
|
+
if (!shared || 'object' != typeof shared) return shared;
|
|
84
|
+
const react = findShared(toEntries(shared), 'react');
|
|
85
|
+
if (!react) return shared;
|
|
86
|
+
return withShared(shared, Object.fromEntries(REACT_JSX_RUNTIMES.map((request)=>[
|
|
87
|
+
request,
|
|
88
|
+
{
|
|
89
|
+
...void 0 === react.requiredVersion ? {} : {
|
|
90
|
+
requiredVersion: react.requiredVersion
|
|
91
|
+
},
|
|
92
|
+
singleton: true,
|
|
93
|
+
treeShaking: false
|
|
94
|
+
}
|
|
95
|
+
])));
|
|
96
|
+
};
|
|
97
|
+
const withFrameworkShared = (shared, packages)=>withShared(shared, Object.fromEntries(packages.flatMap(({ requests, version })=>requests.map((request)=>[
|
|
98
|
+
request,
|
|
99
|
+
{
|
|
100
|
+
requiredVersion: version,
|
|
101
|
+
singleton: true,
|
|
102
|
+
treeShaking: false
|
|
103
|
+
}
|
|
104
|
+
]))));
|
|
105
|
+
const federationConfig = (options)=>options.mfConfig && 'object' == typeof options.mfConfig ? federationConfig(options.mfConfig) : options;
|
|
106
|
+
const withDefaults = (options, packages)=>options.mfConfig && 'object' == typeof options.mfConfig ? {
|
|
107
|
+
...options,
|
|
108
|
+
mfConfig: withDefaults(options.mfConfig, packages)
|
|
109
|
+
} : {
|
|
110
|
+
...options,
|
|
111
|
+
shared: withFrameworkShared(withReactJsxRuntimeShared(options.shared), packages)
|
|
112
|
+
};
|
|
113
|
+
const hasExposes = (options)=>{
|
|
114
|
+
const { exposes } = federationConfig(options);
|
|
115
|
+
return Array.isArray(exposes) ? exposes.length > 0 : Boolean(exposes) && Object.keys(exposes).length > 0;
|
|
116
|
+
};
|
|
117
|
+
const SHARED_MODULE_TYPES = new Set([
|
|
118
|
+
'consume-shared-module',
|
|
119
|
+
'provide-module'
|
|
120
|
+
]);
|
|
121
|
+
const resourceOf = (module)=>module && 'resource' in module && 'string' == typeof module.resource ? module.resource : void 0;
|
|
122
|
+
const describeModule = (module)=>module ? resourceOf(module) ?? module.identifier() : 'an entry';
|
|
123
|
+
const CONTAINER_ENTRY_DEPENDENCY = 'container entry';
|
|
124
|
+
const buildsContainer = (compilation)=>[
|
|
125
|
+
...compilation.entries.values()
|
|
126
|
+
].some(({ dependencies })=>dependencies.some(({ type })=>type === CONTAINER_ENTRY_DEPENDENCY));
|
|
127
|
+
class FederationPrivateContextsPlugin {
|
|
128
|
+
apply(compiler) {
|
|
129
|
+
const name = 'FederationPrivateContextsPlugin';
|
|
130
|
+
const definitionOf = (module)=>{
|
|
131
|
+
const resource = resourceOf(module);
|
|
132
|
+
return void 0 === resource ? void 0 : this.packages.find(({ contexts, directory })=>contexts?.test(resource) && resource.startsWith(directory + node_path.sep));
|
|
133
|
+
};
|
|
134
|
+
compiler.hooks.compilation.tap(name, (compilation)=>{
|
|
135
|
+
compilation.hooks.finishModules.tap(name, (modules)=>{
|
|
136
|
+
if (!buildsContainer(compilation)) return;
|
|
137
|
+
for (const module of modules){
|
|
138
|
+
const share = definitionOf(module);
|
|
139
|
+
if (!share) continue;
|
|
140
|
+
const importer = compilation.moduleGraph.getIncomingConnections(module).map((connection)=>connection.originModule).find((origin)=>!(origin && SHARED_MODULE_TYPES.has(origin.type)) && definitionOf(origin) !== share);
|
|
141
|
+
if (void 0 !== importer) compilation.errors.push(new compiler.webpack.WebpackError(`[ultramodern] This Module Federation build bundles a private copy of ${share.contextsRequest}: ${describeModule(importer)} imports ${describeModule(module)} directly. It would not see the host's request state. Share the "${share.prefix}" subpaths and import the contexts through "${share.contextsRequest}".`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
constructor(packages){
|
|
147
|
+
this.packages = packages;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const ultramodernModuleFederationSharedPlugin = ()=>({
|
|
151
|
+
name: '@modern-js/ultramodern-module-federation-shared',
|
|
152
|
+
pre: [
|
|
153
|
+
'@modern-js/plugin-module-federation',
|
|
154
|
+
'@modern-js/plugin-module-federation-ssr'
|
|
155
|
+
],
|
|
156
|
+
setup (api) {
|
|
157
|
+
api.modifyBundlerChain((chain)=>{
|
|
158
|
+
const ids = MODULE_FEDERATION_CHAIN_IDS.filter((id)=>chain.plugins.has(id));
|
|
159
|
+
if (0 === ids.length) return;
|
|
160
|
+
const packages = resolveFrameworkSharedPackages(api.getAppContext().appDirectory);
|
|
161
|
+
let exposes = false;
|
|
162
|
+
for (const id of ids){
|
|
163
|
+
const [options] = chain.plugin(id).get('args');
|
|
164
|
+
exposes ||= hasExposes(options);
|
|
165
|
+
chain.plugin(id).tap(([options, ...rest])=>[
|
|
166
|
+
withDefaults(options, packages),
|
|
167
|
+
...rest
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
if (exposes) chain.plugin('ultramodern-federation-private-contexts').use(FederationPrivateContextsPlugin, [
|
|
171
|
+
packages
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
export { FederationPrivateContextsPlugin, resolveFrameworkSharedPackages, ultramodernModuleFederationSharedPlugin, withFrameworkShared, withReactJsxRuntimeShared };
|
|
@@ -7,6 +7,7 @@ import { resolveUltramodernReleaseIdentity } from "@modern-js/app-tools-extensio
|
|
|
7
7
|
import { findHostingModuleDirectory } from "@modern-js/app-tools-extensions/runtime-package-resolution";
|
|
8
8
|
import { mergeConfig } from "@modern-js/plugin/cli";
|
|
9
9
|
import { rspack } from "@rsbuild/core";
|
|
10
|
+
import { ultramodernModuleFederationSharedPlugin } from "./module-federation-shared-plugin.mjs";
|
|
10
11
|
const DEFAULT_OTLP_ENDPOINT = 'http://127.0.0.1:4318/v1/logs';
|
|
11
12
|
const DEFAULT_VICTORIA_METRICS_ENDPOINT = 'http://127.0.0.1:8428/api/v1/import/prometheus';
|
|
12
13
|
const resolveReactRouterPackageDir = (appDirectory)=>{
|
|
@@ -67,6 +68,9 @@ const createPresetUltramodernConfig = (options = {})=>{
|
|
|
67
68
|
output: {
|
|
68
69
|
precompress: true
|
|
69
70
|
},
|
|
71
|
+
plugins: [
|
|
72
|
+
ultramodernModuleFederationSharedPlugin()
|
|
73
|
+
],
|
|
70
74
|
server,
|
|
71
75
|
source: {
|
|
72
76
|
reactCompiler: true,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
const REACT_JSX_RUNTIMES = [
|
|
5
|
+
'react/jsx-runtime',
|
|
6
|
+
'react/jsx-dev-runtime'
|
|
7
|
+
];
|
|
8
|
+
const MODULE_FEDERATION_CHAIN_IDS = [
|
|
9
|
+
'plugin-module-federation',
|
|
10
|
+
'plugin-module-federation-server'
|
|
11
|
+
];
|
|
12
|
+
const FRAMEWORK_SHARES = [
|
|
13
|
+
{
|
|
14
|
+
packageName: '@modern-js/runtime',
|
|
15
|
+
prefix: '@modern-js/runtime/',
|
|
16
|
+
contextsRequest: '@modern-js/runtime/context',
|
|
17
|
+
contexts: /[\\/]core[\\/]context[\\/](?:index|runtime)\.[cm]?[jt]sx?$/
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
packageName: '@modern-js/plugin-i18n',
|
|
21
|
+
prefix: '@modern-js/plugin-i18n/runtime/',
|
|
22
|
+
contextsRequest: '@modern-js/plugin-i18n/runtime/contexts',
|
|
23
|
+
contexts: /[\\/]runtime[\\/]contexts\.[cm]?[jt]sx?$/
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
packageName: '@modern-js/bff-effect',
|
|
27
|
+
prefix: '@modern-js/bff-effect/',
|
|
28
|
+
contextsRequest: '@modern-js/bff-effect/context',
|
|
29
|
+
contexts: /[\\/]effect[\\/]context\.[cm]?[jt]sx?$/
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
const loadsCode = (target)=>'string' == typeof target ? !target.endsWith('.d.ts') : Boolean(target) && 'object' == typeof target && Object.entries(target).some(([condition, value])=>'types' !== condition && loadsCode(value));
|
|
33
|
+
const exportedRequests = (packageName, prefix, exports)=>Object.entries(exports && 'object' == typeof exports ? exports : {}).filter(([subpath, target])=>'./package.json' !== subpath && !subpath.includes('*') && loadsCode(target)).map(([subpath])=>`${packageName}${subpath.slice(1)}`).filter((request)=>request.startsWith(prefix)).sort();
|
|
34
|
+
const findInstalledManifest = (appDirectory, packageName)=>{
|
|
35
|
+
for(let directory = appDirectory;;){
|
|
36
|
+
const manifest = node_path.join(directory, 'node_modules', packageName, 'package.json');
|
|
37
|
+
if (existsSync(manifest)) return manifest;
|
|
38
|
+
const parent = node_path.dirname(directory);
|
|
39
|
+
if (parent === directory) return;
|
|
40
|
+
directory = parent;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const resolveFrameworkSharedPackages = (appDirectory)=>FRAMEWORK_SHARES.flatMap((share)=>{
|
|
44
|
+
const manifest = findInstalledManifest(appDirectory, share.packageName);
|
|
45
|
+
if (!manifest) return [];
|
|
46
|
+
const { version, exports } = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
...share,
|
|
50
|
+
version,
|
|
51
|
+
requests: exportedRequests(share.packageName, share.prefix, exports),
|
|
52
|
+
directory: realpathSync(node_path.dirname(manifest))
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
const findShared = (entries, request)=>{
|
|
57
|
+
for (const entry of entries){
|
|
58
|
+
if (entry === request) return {};
|
|
59
|
+
if ('object' == typeof entry && Object.hasOwn(entry, request)) {
|
|
60
|
+
const config = entry[request];
|
|
61
|
+
return 'string' == typeof config ? {
|
|
62
|
+
requiredVersion: config
|
|
63
|
+
} : config;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const toEntries = (shared)=>Array.isArray(shared) ? shared : [
|
|
68
|
+
shared
|
|
69
|
+
];
|
|
70
|
+
const withShared = (shared, missing)=>{
|
|
71
|
+
const entries = toEntries(shared);
|
|
72
|
+
const added = Object.fromEntries(Object.entries(missing).filter(([request])=>!findShared(entries, request)));
|
|
73
|
+
if (0 === Object.keys(added).length) return shared;
|
|
74
|
+
if (!shared) return added;
|
|
75
|
+
return Array.isArray(shared) ? [
|
|
76
|
+
...shared,
|
|
77
|
+
added
|
|
78
|
+
] : {
|
|
79
|
+
...shared,
|
|
80
|
+
...added
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const withReactJsxRuntimeShared = (shared)=>{
|
|
84
|
+
if (!shared || 'object' != typeof shared) return shared;
|
|
85
|
+
const react = findShared(toEntries(shared), 'react');
|
|
86
|
+
if (!react) return shared;
|
|
87
|
+
return withShared(shared, Object.fromEntries(REACT_JSX_RUNTIMES.map((request)=>[
|
|
88
|
+
request,
|
|
89
|
+
{
|
|
90
|
+
...void 0 === react.requiredVersion ? {} : {
|
|
91
|
+
requiredVersion: react.requiredVersion
|
|
92
|
+
},
|
|
93
|
+
singleton: true,
|
|
94
|
+
treeShaking: false
|
|
95
|
+
}
|
|
96
|
+
])));
|
|
97
|
+
};
|
|
98
|
+
const withFrameworkShared = (shared, packages)=>withShared(shared, Object.fromEntries(packages.flatMap(({ requests, version })=>requests.map((request)=>[
|
|
99
|
+
request,
|
|
100
|
+
{
|
|
101
|
+
requiredVersion: version,
|
|
102
|
+
singleton: true,
|
|
103
|
+
treeShaking: false
|
|
104
|
+
}
|
|
105
|
+
]))));
|
|
106
|
+
const federationConfig = (options)=>options.mfConfig && 'object' == typeof options.mfConfig ? federationConfig(options.mfConfig) : options;
|
|
107
|
+
const withDefaults = (options, packages)=>options.mfConfig && 'object' == typeof options.mfConfig ? {
|
|
108
|
+
...options,
|
|
109
|
+
mfConfig: withDefaults(options.mfConfig, packages)
|
|
110
|
+
} : {
|
|
111
|
+
...options,
|
|
112
|
+
shared: withFrameworkShared(withReactJsxRuntimeShared(options.shared), packages)
|
|
113
|
+
};
|
|
114
|
+
const hasExposes = (options)=>{
|
|
115
|
+
const { exposes } = federationConfig(options);
|
|
116
|
+
return Array.isArray(exposes) ? exposes.length > 0 : Boolean(exposes) && Object.keys(exposes).length > 0;
|
|
117
|
+
};
|
|
118
|
+
const SHARED_MODULE_TYPES = new Set([
|
|
119
|
+
'consume-shared-module',
|
|
120
|
+
'provide-module'
|
|
121
|
+
]);
|
|
122
|
+
const resourceOf = (module)=>module && 'resource' in module && 'string' == typeof module.resource ? module.resource : void 0;
|
|
123
|
+
const describeModule = (module)=>module ? resourceOf(module) ?? module.identifier() : 'an entry';
|
|
124
|
+
const CONTAINER_ENTRY_DEPENDENCY = 'container entry';
|
|
125
|
+
const buildsContainer = (compilation)=>[
|
|
126
|
+
...compilation.entries.values()
|
|
127
|
+
].some(({ dependencies })=>dependencies.some(({ type })=>type === CONTAINER_ENTRY_DEPENDENCY));
|
|
128
|
+
class FederationPrivateContextsPlugin {
|
|
129
|
+
apply(compiler) {
|
|
130
|
+
const name = 'FederationPrivateContextsPlugin';
|
|
131
|
+
const definitionOf = (module)=>{
|
|
132
|
+
const resource = resourceOf(module);
|
|
133
|
+
return void 0 === resource ? void 0 : this.packages.find(({ contexts, directory })=>contexts?.test(resource) && resource.startsWith(directory + node_path.sep));
|
|
134
|
+
};
|
|
135
|
+
compiler.hooks.compilation.tap(name, (compilation)=>{
|
|
136
|
+
compilation.hooks.finishModules.tap(name, (modules)=>{
|
|
137
|
+
if (!buildsContainer(compilation)) return;
|
|
138
|
+
for (const module of modules){
|
|
139
|
+
const share = definitionOf(module);
|
|
140
|
+
if (!share) continue;
|
|
141
|
+
const importer = compilation.moduleGraph.getIncomingConnections(module).map((connection)=>connection.originModule).find((origin)=>!(origin && SHARED_MODULE_TYPES.has(origin.type)) && definitionOf(origin) !== share);
|
|
142
|
+
if (void 0 !== importer) compilation.errors.push(new compiler.webpack.WebpackError(`[ultramodern] This Module Federation build bundles a private copy of ${share.contextsRequest}: ${describeModule(importer)} imports ${describeModule(module)} directly. It would not see the host's request state. Share the "${share.prefix}" subpaths and import the contexts through "${share.contextsRequest}".`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
constructor(packages){
|
|
148
|
+
this.packages = packages;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const ultramodernModuleFederationSharedPlugin = ()=>({
|
|
152
|
+
name: '@modern-js/ultramodern-module-federation-shared',
|
|
153
|
+
pre: [
|
|
154
|
+
'@modern-js/plugin-module-federation',
|
|
155
|
+
'@modern-js/plugin-module-federation-ssr'
|
|
156
|
+
],
|
|
157
|
+
setup (api) {
|
|
158
|
+
api.modifyBundlerChain((chain)=>{
|
|
159
|
+
const ids = MODULE_FEDERATION_CHAIN_IDS.filter((id)=>chain.plugins.has(id));
|
|
160
|
+
if (0 === ids.length) return;
|
|
161
|
+
const packages = resolveFrameworkSharedPackages(api.getAppContext().appDirectory);
|
|
162
|
+
let exposes = false;
|
|
163
|
+
for (const id of ids){
|
|
164
|
+
const [options] = chain.plugin(id).get('args');
|
|
165
|
+
exposes ||= hasExposes(options);
|
|
166
|
+
chain.plugin(id).tap(([options, ...rest])=>[
|
|
167
|
+
withDefaults(options, packages),
|
|
168
|
+
...rest
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
if (exposes) chain.plugin('ultramodern-federation-private-contexts').use(FederationPrivateContextsPlugin, [
|
|
172
|
+
packages
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
export { FederationPrivateContextsPlugin, resolveFrameworkSharedPackages, ultramodernModuleFederationSharedPlugin, withFrameworkShared, withReactJsxRuntimeShared };
|
|
@@ -8,6 +8,7 @@ import { resolveUltramodernReleaseIdentity } from "@modern-js/app-tools-extensio
|
|
|
8
8
|
import { findHostingModuleDirectory } from "@modern-js/app-tools-extensions/runtime-package-resolution";
|
|
9
9
|
import { mergeConfig } from "@modern-js/plugin/cli";
|
|
10
10
|
import { rspack } from "@rsbuild/core";
|
|
11
|
+
import { ultramodernModuleFederationSharedPlugin } from "./module-federation-shared-plugin.mjs";
|
|
11
12
|
const DEFAULT_OTLP_ENDPOINT = 'http://127.0.0.1:4318/v1/logs';
|
|
12
13
|
const DEFAULT_VICTORIA_METRICS_ENDPOINT = 'http://127.0.0.1:8428/api/v1/import/prometheus';
|
|
13
14
|
const resolveReactRouterPackageDir = (appDirectory)=>{
|
|
@@ -68,6 +69,9 @@ const createPresetUltramodernConfig = (options = {})=>{
|
|
|
68
69
|
output: {
|
|
69
70
|
precompress: true
|
|
70
71
|
},
|
|
72
|
+
plugins: [
|
|
73
|
+
ultramodernModuleFederationSharedPlugin()
|
|
74
|
+
],
|
|
71
75
|
server,
|
|
72
76
|
source: {
|
|
73
77
|
reactCompiler: true,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
|
+
import type { Rspack } from '@rsbuild/core';
|
|
3
|
+
export type FrameworkSharedPackage = {
|
|
4
|
+
prefix: string;
|
|
5
|
+
version: string;
|
|
6
|
+
/** Exported subpath requests under `prefix` that load code. */
|
|
7
|
+
requests: string[];
|
|
8
|
+
/** Real path of the installed package directory. */
|
|
9
|
+
directory: string;
|
|
10
|
+
contextsRequest?: string;
|
|
11
|
+
contexts?: RegExp;
|
|
12
|
+
};
|
|
13
|
+
/** The framework packages the app installs, with their installed versions. */
|
|
14
|
+
export declare const resolveFrameworkSharedPackages: (appDirectory: string) => FrameworkSharedPackage[];
|
|
15
|
+
/**
|
|
16
|
+
* Share `react/jsx-runtime` and `react/jsx-dev-runtime` as singletons at the
|
|
17
|
+
* shared `react` version whenever `react` itself is shared. Entries the app
|
|
18
|
+
* configured explicitly are kept as written.
|
|
19
|
+
*/
|
|
20
|
+
export declare const withReactJsxRuntimeShared: <T>(shared: T) => T;
|
|
21
|
+
/**
|
|
22
|
+
* Share every exported subpath of the framework packages as a singleton at
|
|
23
|
+
* the installed version. Entries the app configured explicitly are kept.
|
|
24
|
+
*/
|
|
25
|
+
export declare const withFrameworkShared: <T>(shared: T, packages: FrameworkSharedPackage[]) => T;
|
|
26
|
+
/**
|
|
27
|
+
* Fails a federation build that exposes modules when a framework context file
|
|
28
|
+
* is imported other than through its shared request. That remote would run
|
|
29
|
+
* with its own contexts and never see the host's request state.
|
|
30
|
+
*
|
|
31
|
+
* The check runs only when the compilation really builds a container: an
|
|
32
|
+
* environment may drop the federation plugin after the chain was configured
|
|
33
|
+
* (the Cloudflare workerd SSR graph does), and a graph with no container has
|
|
34
|
+
* no host to share contexts with.
|
|
35
|
+
*/
|
|
36
|
+
export declare class FederationPrivateContextsPlugin {
|
|
37
|
+
private readonly packages;
|
|
38
|
+
constructor(packages: FrameworkSharedPackage[]);
|
|
39
|
+
apply(compiler: Rspack.Compiler): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Apply UltraModern's Module Federation share defaults to the federation
|
|
43
|
+
* plugins that `@module-federation/modern-js-v3` registered on the chain.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ultramodernModuleFederationSharedPlugin: () => CliPlugin<AppTools>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
|
+
import type { Rspack } from '@rsbuild/core';
|
|
3
|
+
export type FrameworkSharedPackage = {
|
|
4
|
+
prefix: string;
|
|
5
|
+
version: string;
|
|
6
|
+
/** Exported subpath requests under `prefix` that load code. */
|
|
7
|
+
requests: string[];
|
|
8
|
+
/** Real path of the installed package directory. */
|
|
9
|
+
directory: string;
|
|
10
|
+
contextsRequest?: string;
|
|
11
|
+
contexts?: RegExp;
|
|
12
|
+
};
|
|
13
|
+
/** The framework packages the app installs, with their installed versions. */
|
|
14
|
+
export declare const resolveFrameworkSharedPackages: (appDirectory: string) => FrameworkSharedPackage[];
|
|
15
|
+
/**
|
|
16
|
+
* Share `react/jsx-runtime` and `react/jsx-dev-runtime` as singletons at the
|
|
17
|
+
* shared `react` version whenever `react` itself is shared. Entries the app
|
|
18
|
+
* configured explicitly are kept as written.
|
|
19
|
+
*/
|
|
20
|
+
export declare const withReactJsxRuntimeShared: <T>(shared: T) => T;
|
|
21
|
+
/**
|
|
22
|
+
* Share every exported subpath of the framework packages as a singleton at
|
|
23
|
+
* the installed version. Entries the app configured explicitly are kept.
|
|
24
|
+
*/
|
|
25
|
+
export declare const withFrameworkShared: <T>(shared: T, packages: FrameworkSharedPackage[]) => T;
|
|
26
|
+
/**
|
|
27
|
+
* Fails a federation build that exposes modules when a framework context file
|
|
28
|
+
* is imported other than through its shared request. That remote would run
|
|
29
|
+
* with its own contexts and never see the host's request state.
|
|
30
|
+
*
|
|
31
|
+
* The check runs only when the compilation really builds a container: an
|
|
32
|
+
* environment may drop the federation plugin after the chain was configured
|
|
33
|
+
* (the Cloudflare workerd SSR graph does), and a graph with no container has
|
|
34
|
+
* no host to share contexts with.
|
|
35
|
+
*/
|
|
36
|
+
export declare class FederationPrivateContextsPlugin {
|
|
37
|
+
private readonly packages;
|
|
38
|
+
constructor(packages: FrameworkSharedPackage[]);
|
|
39
|
+
apply(compiler: Rspack.Compiler): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Apply UltraModern's Module Federation share defaults to the federation
|
|
43
|
+
* plugins that `@module-federation/modern-js-v3` registered on the chain.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ultramodernModuleFederationSharedPlugin: () => CliPlugin<AppTools>;
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"directory": "packages/solutions/ultramodern-app-tools"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
|
-
"version": "3.9.0-ultramodern.
|
|
14
|
+
"version": "3.9.0-ultramodern.18",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"src"
|
|
@@ -62,26 +62,26 @@
|
|
|
62
62
|
"node": ">=26.7.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.
|
|
66
|
-
"@modern-js/app-tools-extensions": "npm:@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.
|
|
67
|
-
"@modern-js/i18n-integration": "npm:@bleedingdev/modern-js-i18n-integration@3.9.0-ultramodern.
|
|
68
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.
|
|
69
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.
|
|
70
|
-
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.
|
|
71
|
-
"@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.
|
|
72
|
-
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.
|
|
73
|
-
"@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.
|
|
65
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.18",
|
|
66
|
+
"@modern-js/app-tools-extensions": "npm:@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.18",
|
|
67
|
+
"@modern-js/i18n-integration": "npm:@bleedingdev/modern-js-i18n-integration@3.9.0-ultramodern.18",
|
|
68
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.18",
|
|
69
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.18",
|
|
70
|
+
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.18",
|
|
71
|
+
"@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.18",
|
|
72
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.18",
|
|
73
|
+
"@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.18",
|
|
74
74
|
"@rsbuild/core": "2.2.9"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.
|
|
78
|
-
"@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.
|
|
79
|
-
"@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.
|
|
80
|
-
"@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.
|
|
81
|
-
"@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.
|
|
82
|
-
"@modern-js/server": "npm:@bleedingdev/modern-js-server@3.9.0-ultramodern.
|
|
83
|
-
"@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.
|
|
84
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.
|
|
77
|
+
"@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.18",
|
|
78
|
+
"@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.18",
|
|
79
|
+
"@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.18",
|
|
80
|
+
"@modern-js/plugin-tanstack": "npm:@bleedingdev/modern-js-plugin-tanstack@3.9.0-ultramodern.18",
|
|
81
|
+
"@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.18",
|
|
82
|
+
"@modern-js/server": "npm:@bleedingdev/modern-js-server@3.9.0-ultramodern.18",
|
|
83
|
+
"@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.18",
|
|
84
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.18",
|
|
85
85
|
"@rslib/core": "1.0.2",
|
|
86
86
|
"@scripts/rstest-config": "2.66.0",
|
|
87
87
|
"@types/node": "^26.6.2",
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
4
|
+
import type { Rspack } from '@rsbuild/core';
|
|
5
|
+
|
|
6
|
+
type SharedConfig = { requiredVersion?: unknown } & Record<string, unknown>;
|
|
7
|
+
type SharedRecord = Record<string, string | SharedConfig>;
|
|
8
|
+
type Shared = SharedRecord | Array<string | SharedRecord>;
|
|
9
|
+
|
|
10
|
+
// The compiled JSX in every federated bundle imports these React entries.
|
|
11
|
+
// Sharing `react` without them gives each bundle its own JSX runtime, so a
|
|
12
|
+
// remote's elements reach the host's React with a foreign runtime identity.
|
|
13
|
+
const REACT_JSX_RUNTIMES = ['react/jsx-runtime', 'react/jsx-dev-runtime'];
|
|
14
|
+
|
|
15
|
+
// Chain ids under which `@module-federation/modern-js-v3` registers its
|
|
16
|
+
// browser and server federation plugins.
|
|
17
|
+
const MODULE_FEDERATION_CHAIN_IDS = [
|
|
18
|
+
'plugin-module-federation',
|
|
19
|
+
'plugin-module-federation-server',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Framework packages whose subpaths share module state: React contexts and
|
|
24
|
+
* Effect's request storage. Every exported subpath under `prefix` is shared,
|
|
25
|
+
* and each subpath reaches that state through a package self-reference (for
|
|
26
|
+
* example `@modern-js/runtime/context`), so a remote never evaluates its own
|
|
27
|
+
* copy. Subpaths are listed from the package's `exports` rather than shared by
|
|
28
|
+
* prefix: the app aliases unexported ones such as `@modern-js/runtime/registry`
|
|
29
|
+
* to its own generated modules, which must never be shared.
|
|
30
|
+
* `contexts` matches the files that define that state (React contexts, the
|
|
31
|
+
* Effect request storage): only a shared module or another of those files may
|
|
32
|
+
* import them.
|
|
33
|
+
*/
|
|
34
|
+
const FRAMEWORK_SHARES = [
|
|
35
|
+
{
|
|
36
|
+
packageName: '@modern-js/runtime',
|
|
37
|
+
prefix: '@modern-js/runtime/',
|
|
38
|
+
contextsRequest: '@modern-js/runtime/context',
|
|
39
|
+
contexts: /[\\/]core[\\/]context[\\/](?:index|runtime)\.[cm]?[jt]sx?$/,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
packageName: '@modern-js/plugin-i18n',
|
|
43
|
+
prefix: '@modern-js/plugin-i18n/runtime/',
|
|
44
|
+
contextsRequest: '@modern-js/plugin-i18n/runtime/contexts',
|
|
45
|
+
contexts: /[\\/]runtime[\\/]contexts\.[cm]?[jt]sx?$/,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
packageName: '@modern-js/bff-effect',
|
|
49
|
+
prefix: '@modern-js/bff-effect/',
|
|
50
|
+
contextsRequest: '@modern-js/bff-effect/context',
|
|
51
|
+
contexts: /[\\/]effect[\\/]context\.[cm]?[jt]sx?$/,
|
|
52
|
+
},
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
export type FrameworkSharedPackage = {
|
|
56
|
+
prefix: string;
|
|
57
|
+
version: string;
|
|
58
|
+
/** Exported subpath requests under `prefix` that load code. */
|
|
59
|
+
requests: string[];
|
|
60
|
+
/** Real path of the installed package directory. */
|
|
61
|
+
directory: string;
|
|
62
|
+
contextsRequest?: string;
|
|
63
|
+
contexts?: RegExp;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// A types-only export or a wildcard pattern names no module to share.
|
|
67
|
+
const loadsCode = (target: unknown): boolean =>
|
|
68
|
+
typeof target === 'string'
|
|
69
|
+
? !target.endsWith('.d.ts')
|
|
70
|
+
: Boolean(target) &&
|
|
71
|
+
typeof target === 'object' &&
|
|
72
|
+
Object.entries(target as object).some(
|
|
73
|
+
([condition, value]) => condition !== 'types' && loadsCode(value),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const exportedRequests = (
|
|
77
|
+
packageName: string,
|
|
78
|
+
prefix: string,
|
|
79
|
+
exports: unknown,
|
|
80
|
+
) =>
|
|
81
|
+
Object.entries(
|
|
82
|
+
exports && typeof exports === 'object' ? (exports as object) : {},
|
|
83
|
+
)
|
|
84
|
+
.filter(
|
|
85
|
+
([subpath, target]) =>
|
|
86
|
+
subpath !== './package.json' &&
|
|
87
|
+
!subpath.includes('*') &&
|
|
88
|
+
loadsCode(target),
|
|
89
|
+
)
|
|
90
|
+
.map(([subpath]) => `${packageName}${subpath.slice(1)}`)
|
|
91
|
+
.filter(request => request.startsWith(prefix))
|
|
92
|
+
.sort();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The app's `node_modules` lookup for `packageName`. `NODE_PATH` is ignored:
|
|
96
|
+
* a package the app does not install is not the app's to share.
|
|
97
|
+
*/
|
|
98
|
+
const findInstalledManifest = (appDirectory: string, packageName: string) => {
|
|
99
|
+
for (let directory = appDirectory; ; ) {
|
|
100
|
+
const manifest = path.join(
|
|
101
|
+
directory,
|
|
102
|
+
'node_modules',
|
|
103
|
+
packageName,
|
|
104
|
+
'package.json',
|
|
105
|
+
);
|
|
106
|
+
if (existsSync(manifest)) return manifest;
|
|
107
|
+
const parent = path.dirname(directory);
|
|
108
|
+
if (parent === directory) return undefined;
|
|
109
|
+
directory = parent;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** The framework packages the app installs, with their installed versions. */
|
|
114
|
+
export const resolveFrameworkSharedPackages = (
|
|
115
|
+
appDirectory: string,
|
|
116
|
+
): FrameworkSharedPackage[] =>
|
|
117
|
+
FRAMEWORK_SHARES.flatMap(share => {
|
|
118
|
+
const manifest = findInstalledManifest(appDirectory, share.packageName);
|
|
119
|
+
if (!manifest) return [];
|
|
120
|
+
const { version, exports } = JSON.parse(readFileSync(manifest, 'utf8')) as {
|
|
121
|
+
version: string;
|
|
122
|
+
exports?: unknown;
|
|
123
|
+
};
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
...share,
|
|
127
|
+
version,
|
|
128
|
+
requests: exportedRequests(share.packageName, share.prefix, exports),
|
|
129
|
+
directory: realpathSync(path.dirname(manifest)),
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const findShared = (
|
|
135
|
+
entries: Array<string | SharedRecord>,
|
|
136
|
+
request: string,
|
|
137
|
+
): SharedConfig | undefined => {
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry === request) return {};
|
|
140
|
+
if (typeof entry === 'object' && Object.hasOwn(entry, request)) {
|
|
141
|
+
const config = entry[request];
|
|
142
|
+
return typeof config === 'string' ? { requiredVersion: config } : config;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const toEntries = (shared: unknown) =>
|
|
149
|
+
(Array.isArray(shared) ? shared : [shared]) as Array<string | SharedRecord>;
|
|
150
|
+
|
|
151
|
+
/** Add `missing` to `shared`, keeping every entry the app configured. */
|
|
152
|
+
const withShared = <T>(shared: T, missing: SharedRecord): T => {
|
|
153
|
+
const entries = toEntries(shared);
|
|
154
|
+
const added = Object.fromEntries(
|
|
155
|
+
Object.entries(missing).filter(
|
|
156
|
+
([request]) => !findShared(entries, request),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
if (Object.keys(added).length === 0) return shared;
|
|
160
|
+
if (!shared) return added as T;
|
|
161
|
+
return (
|
|
162
|
+
Array.isArray(shared) ? [...shared, added] : { ...shared, ...added }
|
|
163
|
+
) as T;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Share `react/jsx-runtime` and `react/jsx-dev-runtime` as singletons at the
|
|
168
|
+
* shared `react` version whenever `react` itself is shared. Entries the app
|
|
169
|
+
* configured explicitly are kept as written.
|
|
170
|
+
*/
|
|
171
|
+
export const withReactJsxRuntimeShared = <T>(shared: T): T => {
|
|
172
|
+
if (!shared || typeof shared !== 'object') return shared;
|
|
173
|
+
const react = findShared(toEntries(shared), 'react');
|
|
174
|
+
if (!react) return shared;
|
|
175
|
+
return withShared(
|
|
176
|
+
shared,
|
|
177
|
+
Object.fromEntries(
|
|
178
|
+
REACT_JSX_RUNTIMES.map(request => [
|
|
179
|
+
request,
|
|
180
|
+
{
|
|
181
|
+
...(react.requiredVersion === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { requiredVersion: react.requiredVersion }),
|
|
184
|
+
singleton: true,
|
|
185
|
+
treeShaking: false,
|
|
186
|
+
},
|
|
187
|
+
]),
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Share every exported subpath of the framework packages as a singleton at
|
|
194
|
+
* the installed version. Entries the app configured explicitly are kept.
|
|
195
|
+
*/
|
|
196
|
+
export const withFrameworkShared = <T>(
|
|
197
|
+
shared: T,
|
|
198
|
+
packages: FrameworkSharedPackage[],
|
|
199
|
+
): T =>
|
|
200
|
+
withShared(
|
|
201
|
+
shared,
|
|
202
|
+
Object.fromEntries(
|
|
203
|
+
packages.flatMap(({ requests, version }) =>
|
|
204
|
+
requests.map(request => [
|
|
205
|
+
request,
|
|
206
|
+
{ requiredVersion: version, singleton: true, treeShaking: false },
|
|
207
|
+
]),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
type FederationPluginOptions = {
|
|
213
|
+
shared?: Shared;
|
|
214
|
+
exposes?: unknown;
|
|
215
|
+
mfConfig?: unknown;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// `secondarySharedTreeShaking` wraps the federation config in `mfConfig`.
|
|
219
|
+
const federationConfig = (
|
|
220
|
+
options: FederationPluginOptions,
|
|
221
|
+
): FederationPluginOptions =>
|
|
222
|
+
options.mfConfig && typeof options.mfConfig === 'object'
|
|
223
|
+
? federationConfig(options.mfConfig as FederationPluginOptions)
|
|
224
|
+
: options;
|
|
225
|
+
|
|
226
|
+
const withDefaults = (
|
|
227
|
+
options: FederationPluginOptions,
|
|
228
|
+
packages: FrameworkSharedPackage[],
|
|
229
|
+
): FederationPluginOptions =>
|
|
230
|
+
options.mfConfig && typeof options.mfConfig === 'object'
|
|
231
|
+
? {
|
|
232
|
+
...options,
|
|
233
|
+
mfConfig: withDefaults(
|
|
234
|
+
options.mfConfig as FederationPluginOptions,
|
|
235
|
+
packages,
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
: {
|
|
239
|
+
...options,
|
|
240
|
+
shared: withFrameworkShared(
|
|
241
|
+
withReactJsxRuntimeShared(options.shared),
|
|
242
|
+
packages,
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const hasExposes = (options: FederationPluginOptions) => {
|
|
247
|
+
const { exposes } = federationConfig(options);
|
|
248
|
+
return Array.isArray(exposes)
|
|
249
|
+
? exposes.length > 0
|
|
250
|
+
: Boolean(exposes) && Object.keys(exposes as object).length > 0;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const SHARED_MODULE_TYPES = new Set([
|
|
254
|
+
'consume-shared-module',
|
|
255
|
+
'provide-module',
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
// Only normal modules carry a file resource.
|
|
259
|
+
const resourceOf = (module: Rspack.Module | null) =>
|
|
260
|
+
module && 'resource' in module && typeof module.resource === 'string'
|
|
261
|
+
? module.resource
|
|
262
|
+
: undefined;
|
|
263
|
+
|
|
264
|
+
const describeModule = (module: Rspack.Module | null) =>
|
|
265
|
+
module ? (resourceOf(module) ?? module.identifier()) : 'an entry';
|
|
266
|
+
|
|
267
|
+
// The dependency type of a Module Federation container entry, the same in
|
|
268
|
+
// Rspack and webpack.
|
|
269
|
+
const CONTAINER_ENTRY_DEPENDENCY = 'container entry';
|
|
270
|
+
|
|
271
|
+
const buildsContainer = (compilation: Rspack.Compilation) =>
|
|
272
|
+
[...compilation.entries.values()].some(({ dependencies }) =>
|
|
273
|
+
dependencies.some(({ type }) => type === CONTAINER_ENTRY_DEPENDENCY),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Fails a federation build that exposes modules when a framework context file
|
|
278
|
+
* is imported other than through its shared request. That remote would run
|
|
279
|
+
* with its own contexts and never see the host's request state.
|
|
280
|
+
*
|
|
281
|
+
* The check runs only when the compilation really builds a container: an
|
|
282
|
+
* environment may drop the federation plugin after the chain was configured
|
|
283
|
+
* (the Cloudflare workerd SSR graph does), and a graph with no container has
|
|
284
|
+
* no host to share contexts with.
|
|
285
|
+
*/
|
|
286
|
+
export class FederationPrivateContextsPlugin {
|
|
287
|
+
constructor(private readonly packages: FrameworkSharedPackage[]) {}
|
|
288
|
+
|
|
289
|
+
apply(compiler: Rspack.Compiler) {
|
|
290
|
+
const name = 'FederationPrivateContextsPlugin';
|
|
291
|
+
const definitionOf = (module: Rspack.Module | null) => {
|
|
292
|
+
const resource = resourceOf(module);
|
|
293
|
+
return resource === undefined
|
|
294
|
+
? undefined
|
|
295
|
+
: this.packages.find(
|
|
296
|
+
({ contexts, directory }) =>
|
|
297
|
+
contexts?.test(resource) &&
|
|
298
|
+
resource.startsWith(directory + path.sep),
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
compiler.hooks.compilation.tap(name, compilation => {
|
|
302
|
+
compilation.hooks.finishModules.tap(name, modules => {
|
|
303
|
+
if (!buildsContainer(compilation)) return;
|
|
304
|
+
for (const module of modules) {
|
|
305
|
+
const share = definitionOf(module);
|
|
306
|
+
if (!share) continue;
|
|
307
|
+
const importer = compilation.moduleGraph
|
|
308
|
+
.getIncomingConnections(module)
|
|
309
|
+
.map(connection => connection.originModule)
|
|
310
|
+
.find(
|
|
311
|
+
origin =>
|
|
312
|
+
!(origin && SHARED_MODULE_TYPES.has(origin.type)) &&
|
|
313
|
+
definitionOf(origin) !== share,
|
|
314
|
+
);
|
|
315
|
+
if (importer === undefined) continue;
|
|
316
|
+
compilation.errors.push(
|
|
317
|
+
new compiler.webpack.WebpackError(
|
|
318
|
+
`[ultramodern] This Module Federation build bundles a private copy of ${share.contextsRequest}: ${describeModule(importer)} imports ${describeModule(module)} directly. It would not see the host's request state. Share the "${share.prefix}" subpaths and import the contexts through "${share.contextsRequest}".`,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Apply UltraModern's Module Federation share defaults to the federation
|
|
329
|
+
* plugins that `@module-federation/modern-js-v3` registered on the chain.
|
|
330
|
+
*/
|
|
331
|
+
export const ultramodernModuleFederationSharedPlugin =
|
|
332
|
+
(): CliPlugin<AppTools> => ({
|
|
333
|
+
name: '@modern-js/ultramodern-module-federation-shared',
|
|
334
|
+
pre: [
|
|
335
|
+
'@modern-js/plugin-module-federation',
|
|
336
|
+
'@modern-js/plugin-module-federation-ssr',
|
|
337
|
+
],
|
|
338
|
+
setup(api) {
|
|
339
|
+
api.modifyBundlerChain(chain => {
|
|
340
|
+
const ids = MODULE_FEDERATION_CHAIN_IDS.filter(id =>
|
|
341
|
+
chain.plugins.has(id),
|
|
342
|
+
);
|
|
343
|
+
if (ids.length === 0) return;
|
|
344
|
+
const packages = resolveFrameworkSharedPackages(
|
|
345
|
+
api.getAppContext().appDirectory,
|
|
346
|
+
);
|
|
347
|
+
let exposes = false;
|
|
348
|
+
for (const id of ids) {
|
|
349
|
+
const [options] = chain.plugin(id).get('args') as [
|
|
350
|
+
FederationPluginOptions,
|
|
351
|
+
];
|
|
352
|
+
exposes ||= hasExposes(options);
|
|
353
|
+
chain
|
|
354
|
+
.plugin(id)
|
|
355
|
+
.tap(([options, ...rest]) => [
|
|
356
|
+
withDefaults(options, packages),
|
|
357
|
+
...rest,
|
|
358
|
+
]);
|
|
359
|
+
}
|
|
360
|
+
if (exposes) {
|
|
361
|
+
chain
|
|
362
|
+
.plugin('ultramodern-federation-private-contexts')
|
|
363
|
+
.use(FederationPrivateContextsPlugin, [packages]);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
});
|
|
@@ -7,6 +7,7 @@ import { resolveUltramodernReleaseIdentity } from '@modern-js/app-tools-extensio
|
|
|
7
7
|
import { findHostingModuleDirectory } from '@modern-js/app-tools-extensions/runtime-package-resolution';
|
|
8
8
|
import { mergeConfig } from '@modern-js/plugin/cli';
|
|
9
9
|
import { type RspackChain, rspack } from '@rsbuild/core';
|
|
10
|
+
import { ultramodernModuleFederationSharedPlugin } from './module-federation-shared-plugin';
|
|
10
11
|
import type { AppUserConfig } from './types';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_OTLP_ENDPOINT = 'http://127.0.0.1:4318/v1/logs';
|
|
@@ -214,8 +215,11 @@ export const createPresetUltramodernConfig = (
|
|
|
214
215
|
// Keep build artifacts predictable across apps.
|
|
215
216
|
precompress: true,
|
|
216
217
|
},
|
|
218
|
+
plugins: [ultramodernModuleFederationSharedPlugin()],
|
|
217
219
|
server,
|
|
218
220
|
source: {
|
|
221
|
+
// Client code only: the builder applies React Compiler to `web`
|
|
222
|
+
// environments and never to node/workerSSR/BFF graphs.
|
|
219
223
|
reactCompiler: true,
|
|
220
224
|
...(deliveryUnit
|
|
221
225
|
? {
|