Skip to content

Commit b0929e0

Browse files
committed
lib: fix ESM default export handling for .mjs files in Module Federation
When .mjs files import a default export from a shared/remote module in Module Federation, they receive the ESM namespace object instead of the actual default export value. The solution of this approach is to override the getExportsType() method in ConsumeSharedModule and RemoteModule to always return "dynamic".
1 parent 2eb0d6a commit b0929e0

File tree

9 files changed

+157
-0
lines changed

9 files changed

+157
-0
lines changed

lib/container/RemoteModule.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const RemoteToExternalDependency = require("./RemoteToExternalDependency");
2525
/** @typedef {import("../Module").NeedBuildCallback} NeedBuildCallback */
2626
/** @typedef {import("../Module").NeedBuildContext} NeedBuildContext */
2727
/** @typedef {import("../Module").SourceTypes} SourceTypes */
28+
/** @typedef {import("../ModuleGraph")} ModuleGraph */
29+
/** @typedef {import("../Module").ExportsType} ExportsType */
2830
/** @typedef {import("../RequestShortener")} RequestShortener */
2931
/** @typedef {import("../ResolverFactory").ResolverWithOptions} ResolverWithOptions */
3032
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
@@ -128,6 +130,15 @@ class RemoteModule extends Module {
128130
return REMOTE_AND_SHARE_INIT_TYPES;
129131
}
130132

133+
/**
134+
* @param {ModuleGraph} moduleGraph the module graph
135+
* @param {boolean | undefined} strict the importing module is strict
136+
* @returns {ExportsType} export type
137+
*/
138+
getExportsType(moduleGraph, strict) {
139+
return "dynamic";
140+
}
141+
131142
/**
132143
* @returns {NameForCondition | null} absolute path which should be used for condition matching (usually the resource path)
133144
*/

lib/sharing/ConsumeSharedModule.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const ConsumeSharedFallbackDependency = require("./ConsumeSharedFallbackDependen
2828
/** @typedef {import("../Module").NeedBuildCallback} NeedBuildCallback */
2929
/** @typedef {import("../Module").NeedBuildContext} NeedBuildContext */
3030
/** @typedef {import("../Module").SourceTypes} SourceTypes */
31+
/** @typedef {import("../ModuleGraph")} ModuleGraph */
32+
/** @typedef {import("../Module").ExportsType} ExportsType */
3133
/** @typedef {import("../RequestShortener")} RequestShortener */
3234
/** @typedef {import("../ResolverFactory").ResolverWithOptions} ResolverWithOptions */
3335
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
@@ -153,6 +155,15 @@ class ConsumeSharedModule extends Module {
153155
return CONSUME_SHARED_TYPES;
154156
}
155157

158+
/**
159+
* @param {ModuleGraph} moduleGraph the module graph
160+
* @param {boolean | undefined} strict the importing module is strict
161+
* @returns {ExportsType} export type
162+
*/
163+
getExportsType(moduleGraph, strict) {
164+
return "dynamic";
165+
}
166+
156167
/**
157168
* @param {string=} type the source type for which the size should be estimated
158169
* @returns {number} the estimated size of the module (must be non-zero)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use strict";
2+
3+
const ConsumeSharedModule = require("../lib/sharing/ConsumeSharedModule");
4+
5+
describe("ConsumeSharedModule", () => {
6+
let module;
7+
const context = "/some/context";
8+
const options = {
9+
shareKey: "my-package",
10+
shareScope: "default",
11+
import: false,
12+
importResolved: undefined,
13+
requiredVersion: undefined,
14+
strictVersion: false,
15+
singleton: false,
16+
eager: false
17+
};
18+
19+
beforeEach(() => {
20+
module = new ConsumeSharedModule(context, options);
21+
module.buildMeta = {};
22+
module.buildInfo = {};
23+
});
24+
25+
describe("#getExportsType", () => {
26+
it('returns "dynamic" regardless of strict parameter', () => {
27+
const mockModuleGraph = {};
28+
29+
expect(module.getExportsType(mockModuleGraph, false)).toBe("dynamic");
30+
31+
expect(module.getExportsType(mockModuleGraph, true)).toBe("dynamic");
32+
});
33+
34+
it('returns "dynamic" to enable runtime __esModule check', () => {
35+
const mockModuleGraph = {};
36+
const exportsType = module.getExportsType(mockModuleGraph, true);
37+
38+
expect(exportsType).toBe("dynamic");
39+
});
40+
});
41+
});

test/RemoteModule.unittest.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use strict";
2+
3+
const RemoteModule = require("../lib/container/RemoteModule");
4+
5+
describe("RemoteModule", () => {
6+
let module;
7+
const request = "my-remote/module";
8+
const externalRequests = ["remote1@https://example.com/remote1.js"];
9+
const internalRequest = "./module";
10+
const shareScope = "default";
11+
12+
beforeEach(() => {
13+
module = new RemoteModule(
14+
request,
15+
externalRequests,
16+
internalRequest,
17+
shareScope
18+
);
19+
module.buildMeta = {};
20+
module.buildInfo = { strict: true };
21+
});
22+
23+
describe("#getExportsType", () => {
24+
it('returns "dynamic" regardless of strict parameter', () => {
25+
const mockModuleGraph = {};
26+
27+
expect(module.getExportsType(mockModuleGraph, false)).toBe("dynamic");
28+
29+
expect(module.getExportsType(mockModuleGraph, true)).toBe("dynamic");
30+
});
31+
32+
it('returns "dynamic" to enable runtime __esModule check', () => {
33+
const mockModuleGraph = {};
34+
const exportsType = module.getExportsType(mockModuleGraph, true);
35+
36+
expect(exportsType).toBe("dynamic");
37+
});
38+
});
39+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
it("should correctly handle default imports in .mjs files from shared modules", async () => {
2+
await __webpack_init_sharing__("default");
3+
4+
const { testDefaultImport } = await import("./pure-esm-consumer.mjs");
5+
const result = testDefaultImport();
6+
7+
expect(result.defaultType).toBe("function");
8+
expect(result.defaultValue).toBe("shared default export");
9+
expect(result.namedExportValue).toBe("shared named export");
10+
});

test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/index.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import something from "shared-esm-pkg";
2+
import { namedExport } from "shared-esm-pkg";
3+
4+
export function testDefaultImport() {
5+
return {
6+
defaultType: typeof something,
7+
defaultValue: typeof something === "function" ? something() : something,
8+
namedExportValue: namedExport
9+
};
10+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use strict";
2+
3+
const { ConsumeSharedPlugin, ProvideSharedPlugin } =
4+
require("../../../../").sharing;
5+
6+
/** @type {import("../../../../").Configuration} */
7+
module.exports = {
8+
mode: "development",
9+
plugins: [
10+
new ProvideSharedPlugin({
11+
provides: {
12+
"shared-esm-pkg": {
13+
version: "1.0.0"
14+
}
15+
}
16+
}),
17+
new ConsumeSharedPlugin({
18+
consumes: {
19+
"shared-esm-pkg": {
20+
requiredVersion: "^1.0.0"
21+
}
22+
}
23+
})
24+
]
25+
};

0 commit comments

Comments
 (0)