diff --git a/.changeset/early-eggs-attack.md b/.changeset/early-eggs-attack.md new file mode 100644 index 00000000000..fedadcdf047 --- /dev/null +++ b/.changeset/early-eggs-attack.md @@ -0,0 +1,5 @@ +--- +'@module-federation/enhanced': patch +--- + +Fix ESM default export handling for .mjs files by overriding getExportsType() in ConsumeSharedModule and RemoteModule to return "dynamic" diff --git a/packages/enhanced/src/lib/container/RemoteModule.ts b/packages/enhanced/src/lib/container/RemoteModule.ts index 73e2ede44b1..74844f2452f 100644 --- a/packages/enhanced/src/lib/container/RemoteModule.ts +++ b/packages/enhanced/src/lib/container/RemoteModule.ts @@ -147,6 +147,22 @@ class RemoteModule extends Module { return 6; } + /** + * @returns {string} the export type + * + * "dynamic" means: Check at runtime if __esModule is set. + * When set: namespace = { ...exports, default: exports } + * When not set: namespace = { default: exports } + */ + // @ts-ignore + override getExportsType(): + | 'namespace' + | 'default-only' + | 'default-with-named' + | 'dynamic' { + return 'dynamic'; + } + /** * @returns {Set} types available (do not mutate) */ diff --git a/packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts b/packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts index 94a29b0b488..b37936dae97 100644 --- a/packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts +++ b/packages/enhanced/src/lib/sharing/ConsumeSharedModule.ts @@ -205,6 +205,22 @@ class ConsumeSharedModule extends Module { return 42; } + /** + * @returns {string} the export type + * + * "dynamic" means: Check at runtime if __esModule is set. + * When set: namespace = { ...exports, default: exports } + * When not set: namespace = { default: exports } + */ + // @ts-ignore + override getExportsType(): + | 'namespace' + | 'default-only' + | 'default-with-named' + | 'dynamic' { + return 'dynamic'; + } + /** * @param {Hash} hash the hash used to track dependencies * @param {UpdateHashContext} context context diff --git a/packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts b/packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts index 7ca621558a5..c015d1e5c29 100644 --- a/packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts +++ b/packages/enhanced/src/lib/sharing/ConsumeSharedPlugin.ts @@ -696,57 +696,6 @@ class ConsumeSharedPlugin { }, ); - // Add finishModules hook to copy buildMeta/buildInfo from fallback modules *after* webpack's export analysis - // Running earlier causes failures, so we intentionally execute later than plugins like FlagDependencyExportsPlugin. - // This still follows webpack's pattern used by FlagDependencyExportsPlugin and InferAsyncModulesPlugin, but with a - // later stage. Based on webpack's Compilation.js: finishModules (line 2833) runs before seal (line 2920). - compilation.hooks.finishModules.tapAsync( - { - name: PLUGIN_NAME, - stage: 10, // Run after FlagDependencyExportsPlugin (default stage 0) - }, - (modules, callback) => { - for (const module of modules) { - // Only process ConsumeSharedModule instances with fallback dependencies - if ( - !(module instanceof ConsumeSharedModule) || - !module.options.import - ) { - continue; - } - - let dependency; - if (module.options.eager) { - // For eager mode, get the fallback directly from dependencies - dependency = module.dependencies[0]; - } else { - // For async mode, get it from the async dependencies block - dependency = module.blocks[0]?.dependencies[0]; - } - - if (dependency) { - const fallbackModule = - compilation.moduleGraph.getModule(dependency); - if ( - fallbackModule && - fallbackModule.buildMeta && - fallbackModule.buildInfo - ) { - // Copy buildMeta and buildInfo following webpack's DelegatedModule pattern: this.buildMeta = { ...delegateData.buildMeta }; - // This ensures ConsumeSharedModule inherits ESM/CJS detection (exportsType) and other optimization metadata - module.buildMeta = { ...fallbackModule.buildMeta }; - module.buildInfo = { ...fallbackModule.buildInfo }; - // Mark all exports as provided, to avoid webpack's export analysis from marking them as unused since we copy buildMeta - compilation.moduleGraph - .getExportsInfo(module) - .setUnknownExportsProvided(); - } - } - } - callback(); - }, - ); - compilation.hooks.additionalTreeRuntimeRequirements.tap( PLUGIN_NAME, (chunk, set) => { diff --git a/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/index.js b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/index.js new file mode 100644 index 00000000000..f9ff030a0b8 --- /dev/null +++ b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/index.js @@ -0,0 +1,7 @@ +it('should correctly handle default imports in .mjs files from remote modules', async () => { + const { testDefaultImport } = await import('./pure-esm-consumer.mjs'); + const result = testDefaultImport(); + expect(result.defaultType).toBe('function'); + expect(result.defaultValue).toBe('remote default export'); + expect(result.namedExportValue).toBe('remote named export'); +}); diff --git a/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/pure-esm-consumer.mjs b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/pure-esm-consumer.mjs new file mode 100644 index 00000000000..0a74e985ca9 --- /dev/null +++ b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/pure-esm-consumer.mjs @@ -0,0 +1,10 @@ +import something from 'remote-esm-pkg/module'; +import { namedExport } from 'remote-esm-pkg/module'; + +export function testDefaultImport() { + return { + defaultType: typeof something, + defaultValue: typeof something === 'function' ? something() : something, + namedExportValue: namedExport, + }; +} diff --git a/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/test.config.js b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/test.config.js new file mode 100644 index 00000000000..468f8fed935 --- /dev/null +++ b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/test.config.js @@ -0,0 +1,19 @@ +module.exports = { + moduleScope(scope) { + scope.REMOTE_ESM_PKG = { + get(module) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(() => ({ + __esModule: true, + default: function remoteFunction() { + return 'remote default export'; + }, + namedExport: 'remote named export', + })); + }, 100); + }); + }, + }; + }, +}; diff --git a/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/webpack.config.js b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/webpack.config.js new file mode 100644 index 00000000000..e351532ca0f --- /dev/null +++ b/packages/enhanced/test/configCases/container/remote-module-mjs-default-export/webpack.config.js @@ -0,0 +1,14 @@ +const { ContainerReferencePlugin } = require('../../../../dist/src'); + +module.exports = { + mode: 'development', + devtool: false, + plugins: [ + new ContainerReferencePlugin({ + remoteType: 'var', + remotes: { + 'remote-esm-pkg': 'REMOTE_ESM_PKG', + }, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/index.js b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/index.js new file mode 100644 index 00000000000..df01290358b --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/index.js @@ -0,0 +1,8 @@ +it('should correctly handle default imports in .mjs files from shared modules', async () => { + await __webpack_init_sharing__('default'); + const { testDefaultImport } = await import('./pure-esm-consumer.mjs'); + const result = testDefaultImport(); + expect(result.defaultType).toBe('function'); + expect(result.defaultValue).toBe('shared default export'); + expect(result.namedExportValue).toBe('shared named export'); +}); diff --git a/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/index.js b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/index.js new file mode 100644 index 00000000000..7512ee45979 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/index.js @@ -0,0 +1,5 @@ +export default function sharedFunction() { + return 'shared default export'; +} + +export const namedExport = 'shared named export'; diff --git a/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/package.json b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/package.json new file mode 100644 index 00000000000..7fac6b2a9f8 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/node_modules/shared-esm-pkg/package.json @@ -0,0 +1,5 @@ +{ + "name": "shared-esm-pkg", + "version": "1.0.0", + "type": "module" +} diff --git a/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/pure-esm-consumer.mjs b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/pure-esm-consumer.mjs new file mode 100644 index 00000000000..b26e0cfe3cc --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/pure-esm-consumer.mjs @@ -0,0 +1,10 @@ +import something from 'shared-esm-pkg'; +import { namedExport } from 'shared-esm-pkg'; + +export function testDefaultImport() { + return { + defaultType: typeof something, + defaultValue: typeof something === 'function' ? something() : something, + namedExportValue: namedExport, + }; +} diff --git a/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/webpack.config.js b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/webpack.config.js new file mode 100644 index 00000000000..85b2a931fc0 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/consume-module-mjs-default-export/webpack.config.js @@ -0,0 +1,30 @@ +const { + ConsumeSharedPlugin, + ProvideSharedPlugin, +} = require('../../../../dist/src'); + +module.exports = { + mode: 'development', + devtool: false, + plugins: [ + new ProvideSharedPlugin({ + provides: { + 'shared-esm-pkg': { + shareKey: 'shared-esm-pkg', + version: '1.0.0', + eager: true, + }, + }, + }), + new ConsumeSharedPlugin({ + consumes: { + 'shared-esm-pkg': { + shareKey: 'shared-esm-pkg', + requiredVersion: '^1.0.0', + strictVersion: false, + eager: true, + }, + }, + }), + ], +};