Skip to content

Commit b44b75e

Browse files
authored
feat(browser): support ModuleFederationPlugin (#11558)
* Bundle mf runtime * getPaths * MF runtime * parse_url_as_http * Remove
1 parent 6e77584 commit b44b75e

File tree

3 files changed

+142
-84
lines changed

3 files changed

+142
-84
lines changed

crates/rspack_plugin_schemes/src/http_uri/mod.rs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ async fn resolve_content(
5959
}
6060
}
6161
}
62+
63+
/// Scheme only for http and https
64+
fn parse_url_as_http(url: &str) -> Option<Url> {
65+
let url = Url::parse(url).ok()?;
66+
if url.scheme() != "http" && url.scheme() != "https" {
67+
return None;
68+
}
69+
Some(url)
70+
}
71+
6272
impl HttpUriPlugin {
6373
pub fn new(options: HttpUriPluginOptions) -> Self {
6474
Self::new_inner(options)
@@ -108,16 +118,17 @@ async fn resolve_for_scheme(
108118
_scheme: &Scheme,
109119
) -> Result<Option<bool>> {
110120
// Try to parse the URL and handle it
111-
match Url::parse(&resource_data.resource) {
112-
Ok(url) => match self
113-
.respond_with_url_module(resource_data, &url, None)
114-
.await
115-
{
116-
Ok(true) => Ok(Some(true)),
117-
Ok(false) => Ok(None),
118-
Err(e) => Err(e),
119-
},
120-
Err(_) => Ok(None),
121+
let Some(url) = parse_url_as_http(&resource_data.resource) else {
122+
return Ok(None);
123+
};
124+
125+
match self
126+
.respond_with_url_module(resource_data, &url, None)
127+
.await
128+
{
129+
Ok(true) => Ok(Some(true)),
130+
Ok(false) => Ok(None),
131+
Err(e) => Err(e),
121132
}
122133
}
123134

@@ -147,9 +158,8 @@ async fn resolve_in_scheme(
147158
}
148159

149160
// Parse the base URL from context
150-
let base_url = match Url::parse(&format!("{}/", data.context)) {
151-
Ok(url) => url,
152-
Err(_) => return Ok(None),
161+
let Some(base_url) = parse_url_as_http(&format!("{}/", data.context)) else {
162+
return Ok(None);
153163
};
154164

155165
// Join the base URL with the resource
@@ -237,20 +247,20 @@ impl HttpUriOptionsAllowedUris {
237247
// set
238248
fn get_resource_context(result_entry_resolved: &str) -> Option<String> {
239249
// Parse the resolved URL
240-
if let Ok(base_url) = Url::parse(result_entry_resolved) {
241-
// Resolve the relative path "." against the base URL
242-
if let Ok(resolved_url) = base_url.join(".") {
243-
// Convert the resolved URL to a string
244-
let mut href = resolved_url.to_string();
250+
let base_url = parse_url_as_http(result_entry_resolved)?;
245251

246-
// Remove the trailing slash if it exists
247-
if href.ends_with('/') {
248-
href.pop();
249-
}
252+
// Resolve the relative path "." against the base URL
253+
if let Ok(resolved_url) = base_url.join(".") {
254+
// Convert the resolved URL to a string
255+
let mut href = resolved_url.to_string();
250256

251-
// Return the context as a string
252-
return Some(href);
257+
// Remove the trailing slash if it exists
258+
if href.ends_with('/') {
259+
href.pop();
253260
}
261+
262+
// Return the context as a string
263+
return Some(href);
254264
}
255265

256266
// Return None if parsing or joining fails

packages/rspack/rslib.browser.config.ts

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,13 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3+
import vm from "node:vm";
34
import { pluginNodePolyfill } from "@rsbuild/plugin-node-polyfill";
4-
import { defineConfig, type rsbuild } from "@rslib/core";
5+
import { defineConfig, type rsbuild, rspack } from "@rslib/core";
56

67
const bindingDir = path.resolve("../../crates/node_binding");
78
const distDir = path.resolve("../rspack-browser/dist");
89

9-
/**
10-
* Since `@rspack/browser` doesn't depend on `@rspack/binding`, we should directly bundle the type declarations to it.
11-
* This plugin will replace the usages of `@rspack/binding` to the relative dts path in the generated .d.ts files.
12-
* The `binding.d.ts` and the `napi.binding.d.ts` will be copied to the output directory with RspackCopyPlugin.
13-
*
14-
* The reason that we don't use `paths` in `tsconfig.json` is that it can't rewrite the module idents in `declare module`,
15-
* so we decided to simply replace all instances of it.
16-
*/
17-
const replaceDtsPlugin: rsbuild.RsbuildPlugin = {
18-
name: "replace-dts-plugin",
19-
setup(api) {
20-
api.onAfterBuild(async () => {
21-
const outFiles = await fs.readdir(distDir, { recursive: true });
22-
for (const file of outFiles) {
23-
// Filter *.d.ts
24-
if (!file.endsWith(".d.ts")) {
25-
continue;
26-
}
27-
const filePath = path.join(distDir, file);
28-
29-
const dts = (await fs.readFile(filePath)).toString();
30-
let relativeBindingDts = path.relative(
31-
path.dirname(filePath),
32-
path.join(distDir, "binding")
33-
);
34-
35-
// Ensure relative path starts with "./"
36-
if (!relativeBindingDts.startsWith("../")) {
37-
relativeBindingDts = `./${relativeBindingDts}`;
38-
}
39-
40-
// There are three cases that @rspack/binding may be used
41-
// 1. import("@rspack/binding").XXX
42-
// 2. import { XX } from "@rspack/binding"
43-
// 3. declare module "@rspack/binding" { XX }
44-
const replacedDts = dts
45-
.replaceAll(
46-
'import("@rspack/binding")',
47-
`import("${relativeBindingDts}")`
48-
)
49-
.replaceAll('from "@rspack/binding"', `from "${relativeBindingDts}"`)
50-
.replaceAll(
51-
'declare module "@rspack/binding"',
52-
`declare module "${relativeBindingDts}"`
53-
);
54-
await fs.writeFile(filePath, replacedDts);
55-
}
56-
});
57-
}
58-
};
10+
const MF_RUNTIME_CODE = await getModuleFederationRuntimeCode();
5911

6012
export default defineConfig({
6113
resolve: {
@@ -123,7 +75,7 @@ export default defineConfig({
12375
buffer: path.resolve("./src/browser/buffer")
12476
}
12577
}),
126-
replaceDtsPlugin
78+
replaceDtsPlugin()
12779
],
12880
source: {
12981
tsconfigPath: "./tsconfig.browser.json",
@@ -133,7 +85,9 @@ export default defineConfig({
13385
IS_BROWSER: JSON.stringify(true),
13486
// In `@rspack/browser`, runtime code like loaders and hmr should be written into something like memfs ahead of time.
13587
// Requiring these files should resolve to `@rspack/browser/xx`
136-
__dirname: JSON.stringify("@rspack/browser")
88+
__dirname: JSON.stringify("@rspack/browser"),
89+
// Runtime code
90+
MF_RUNTIME_CODE: JSON.stringify(MF_RUNTIME_CODE)
13791
}
13892
},
13993
tools: {
@@ -157,3 +111,88 @@ export default defineConfig({
157111
}
158112
}
159113
});
114+
115+
/**
116+
* Since `@rspack/browser` doesn't depend on `@rspack/binding`, we should directly bundle the type declarations to it.
117+
* This plugin will replace the usages of `@rspack/binding` to the relative dts path in the generated .d.ts files.
118+
* The `binding.d.ts` and the `napi.binding.d.ts` will be copied to the output directory with RspackCopyPlugin.
119+
*
120+
* The reason that we don't use `paths` in `tsconfig.json` is that it can't rewrite the module idents in `declare module`,
121+
* so we decided to simply replace all instances of it.
122+
*/
123+
function replaceDtsPlugin(): rsbuild.RsbuildPlugin {
124+
return {
125+
name: "replace-dts-plugin",
126+
setup(api) {
127+
api.onAfterBuild(async () => {
128+
const outFiles = await fs.readdir(distDir, { recursive: true });
129+
for (const file of outFiles) {
130+
// Filter *.d.ts
131+
if (!file.endsWith(".d.ts")) {
132+
continue;
133+
}
134+
const filePath = path.join(distDir, file);
135+
136+
const dts = (await fs.readFile(filePath)).toString();
137+
let relativeBindingDts = path.relative(
138+
path.dirname(filePath),
139+
path.join(distDir, "binding")
140+
);
141+
142+
// Ensure relative path starts with "./"
143+
if (!relativeBindingDts.startsWith("../")) {
144+
relativeBindingDts = `./${relativeBindingDts}`;
145+
}
146+
147+
// There are three cases that @rspack/binding may be used
148+
// 1. import("@rspack/binding").XXX
149+
// 2. import { XX } from "@rspack/binding"
150+
// 3. declare module "@rspack/binding" { XX }
151+
const replacedDts = dts
152+
.replaceAll(
153+
'import("@rspack/binding")',
154+
`import("${relativeBindingDts}")`
155+
)
156+
.replaceAll(
157+
'from "@rspack/binding"',
158+
`from "${relativeBindingDts}"`
159+
)
160+
.replaceAll(
161+
'declare module "@rspack/binding"',
162+
`declare module "${relativeBindingDts}"`
163+
);
164+
await fs.writeFile(filePath, replacedDts);
165+
}
166+
});
167+
}
168+
};
169+
}
170+
171+
async function getModuleFederationRuntimeCode() {
172+
const { swc } = rspack.experiments;
173+
const runtime = await fs.readFile(
174+
path.resolve(__dirname, "src/runtime/moduleFederationDefaultRuntime.js"),
175+
"utf-8"
176+
);
177+
178+
const { code: downgradedRuntime } = await swc.transform(runtime, {
179+
jsc: {
180+
target: "es2015"
181+
}
182+
});
183+
184+
const minimizedRuntime = await swc.minify(downgradedRuntime, {
185+
compress: false,
186+
mangle: false,
187+
ecma: 2015
188+
});
189+
190+
const sandbox = { module: { exports: undefined } } as any;
191+
vm.createContext(sandbox);
192+
vm.runInContext(minimizedRuntime.code, sandbox);
193+
194+
const functionContent = rspack.Template.getFunctionContent(
195+
sandbox.module.exports
196+
);
197+
return functionContent;
198+
}

packages/rspack/src/container/ModuleFederationPlugin.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { ModuleFederationPluginV1Options } from "./ModuleFederationPluginV1
66
import { ModuleFederationRuntimePlugin } from "./ModuleFederationRuntimePlugin";
77
import { parseOptions } from "./options";
88

9+
declare const MF_RUNTIME_CODE: string;
10+
911
export interface ModuleFederationPluginOptions
1012
extends Omit<ModuleFederationPluginV1Options, "enhanced"> {
1113
runtimePlugins?: RuntimePlugins;
@@ -142,6 +144,14 @@ function getRuntimePlugins(options: ModuleFederationPluginOptions) {
142144
}
143145

144146
function getPaths(options: ModuleFederationPluginOptions): RuntimePaths {
147+
if (IS_BROWSER) {
148+
return {
149+
runtimeTools: "@module-federation/runtime-tools",
150+
bundlerRuntime: "@module-federation/webpack-bundler-runtime",
151+
runtime: "@module-federation/runtime"
152+
};
153+
}
154+
145155
const runtimeToolsPath =
146156
options.implementation ??
147157
require.resolve("@module-federation/runtime-tools");
@@ -175,6 +185,7 @@ function getDefaultEntryRuntime(
175185
);
176186
runtimePluginVars.push(`${runtimePluginVar}()`);
177187
}
188+
178189
const content = [
179190
`import __module_federation_bundler_runtime__ from ${JSON.stringify(
180191
paths.bundlerRuntime
@@ -190,13 +201,11 @@ function getDefaultEntryRuntime(
190201
`const __module_federation_share_strategy__ = ${JSON.stringify(
191202
options.shareStrategy ?? "version-first"
192203
)}`,
193-
compiler.webpack.Template.getFunctionContent(
194-
IS_BROWSER
195-
? compiler.__internal_browser_require(
196-
"@rspack/browser/moduleFederationDefaultRuntime.js"
197-
)
198-
: require("./moduleFederationDefaultRuntime.js")
199-
)
204+
IS_BROWSER
205+
? MF_RUNTIME_CODE
206+
: compiler.webpack.Template.getFunctionContent(
207+
require("./moduleFederationDefaultRuntime.js")
208+
)
200209
].join(";");
201210
return `@module-federation/runtime/rspack.js!=!data:text/javascript,${content}`;
202211
}

0 commit comments

Comments
 (0)