Skip to content

Commit 50e0997

Browse files
authored
fix: ESM default export handling for .mjs files in Module Federation (#20189)
1 parent 557582a commit 50e0997

File tree

8 files changed

+114
-0
lines changed

8 files changed

+114
-0
lines changed

.changeset/brown-apricots-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Fixed ESM default export handling for `.mjs` files in Module Federation

lib/container/RemoteModule.js

Lines changed: 15 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,19 @@ 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+
* "namespace": Exports is already a namespace object. namespace = exports.
138+
* "dynamic": Check at runtime if __esModule is set. When set: namespace = { ...exports, default: exports }. When not set: namespace = { default: exports }.
139+
* "default-only": Provide a namespace object with only default export. namespace = { default: exports }
140+
* "default-with-named": Provide a namespace object with named and default export. namespace = { ...exports, default: exports }
141+
*/
142+
getExportsType(moduleGraph, strict) {
143+
return "dynamic";
144+
}
145+
131146
/**
132147
* @returns {NameForCondition | null} absolute path which should be used for condition matching (usually the resource path)
133148
*/

lib/sharing/ConsumeSharedModule.js

Lines changed: 39 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,43 @@ 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+
* "namespace": Exports is already a namespace object. namespace = exports.
163+
* "dynamic": Check at runtime if __esModule is set. When set: namespace = { ...exports, default: exports }. When not set: namespace = { default: exports }.
164+
* "default-only": Provide a namespace object with only default export. namespace = { default: exports }
165+
* "default-with-named": Provide a namespace object with named and default export. namespace = { ...exports, default: exports }
166+
*/
167+
getExportsType(moduleGraph, strict) {
168+
if (this.options.import) {
169+
let fallbackModule = null;
170+
171+
if (this.options.eager) {
172+
const dep = this.dependencies[0];
173+
if (dep) {
174+
fallbackModule = moduleGraph.getModule(dep);
175+
}
176+
} else {
177+
const block = this.blocks[0];
178+
if (block && block.dependencies.length > 0) {
179+
fallbackModule = moduleGraph.getModule(block.dependencies[0]);
180+
}
181+
}
182+
183+
if (
184+
fallbackModule &&
185+
fallbackModule.buildMeta &&
186+
fallbackModule.buildMeta.exportsType === "namespace"
187+
) {
188+
return "namespace";
189+
}
190+
}
191+
192+
return "dynamic";
193+
}
194+
156195
/**
157196
* @param {string=} type the source type for which the size should be estimated
158197
* @returns {number} the estimated size of the module (must be non-zero)
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)