Repository navigation
Description
Feature request
Module Federation currently forces the user to manually implement a import() in their user code in order to avoid eager consumption errors.
Webpack should internalize the async boundary in the entrypoint startup or use require.X for active chunk startup, this ensures that chunk handlers are called for subsequent chunks and ideally for the entrypoint itself.
What is the expected behavior?
I should not need to import() in my codebase manually.
My entrypoint should behave like a async chunk load, and call ensureChunkHandlers for itself and any chunks it depends on before evaluating the EntryDependencies.
What is motivation or use case for adding/changing the behavior?
Most frameworks control how entrypoint is loaded, like next.js - there is no way for user to alter the startup of these apps from the outside. the eager consumption issue is a yearslong complaint from the community with most who just start getting stuck for hours wondering what is wrong, not realizing that the import() is critical to operations.
How should this be implemented in your opinion?
We should add a runtime requirement to any entrypoint whose tree contains module federation dependent modules, like shared module or remote module consumption that is not wrapped in AsyncDependencyBlock
In StartupChunkDependenciesPlugin or example.
compilation.hooks.additionalChunkRuntimeRequirements.tap(
'MfStartupChunkDependenciesPlugin',
(chunk, set, { chunkGraph }) => {
if (!isEnabledForChunk(chunk)) return;
if (chunkGraph.getNumberOfEntryModules(chunk) === 0) return;
set.add(federationStartup);
},
);in StartupHelpers + ESM startup / renderStartup hook.
if (federation) {
const chunkIds = Array.from(chunks, (c: Chunk) => c.id);
const wrappedInit = (body: string) =>
Template.asString([
'Promise.all([',
Template.indent([
// may have other chunks who depend on federation, so best to just fallback
// instead of try to figure out if consumes or remotes exists during build
`${RuntimeGlobals.ensureChunkHandlers}.consumes || function(chunkId, promises) {},`,
`${RuntimeGlobals.ensureChunkHandlers}.remotes || function(chunkId, promises) {},`,
]),
`].reduce(${runtimeTemplate.returningFunction(`handler('${chunk.id}', p), p`, 'p, handler')}, promises)`,
`).then(${runtimeTemplate.returningFunction(body)});`,
]);
const wrap = wrappedInit(
`${
passive
? RuntimeGlobals.onChunksLoaded
: RuntimeGlobals.startupEntrypoint
}(0, ${JSON.stringify(chunkIds)}, ${fn})`,
);
runtime.push(`${final && !passive ? EXPORT_PREFIX : ''}${wrap}`);
} else {
const chunkIds = Array.from(chunks, (c: Chunk) => c.id);
runtime.push(
`${final && !passive ? EXPORT_PREFIX : ''}${
passive
? RuntimeGlobals.onChunksLoaded
: RuntimeGlobals.startupEntrypoint
}(0, ${JSON.stringify(chunkIds)}, ${fn});`,
);
if (final && passive) {
runtime.push(`${EXPORT_PREFIX}${RuntimeGlobals.onChunksLoaded}();`);
}
}The output of a entrypoint should look like this:
// load runtime
var __webpack_require__ = require("../webpack-runtime.js");
__webpack_require__.C(exports);
var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var promises = [];
var __webpack_exports__ = Promise.all([
__webpack_require__.f.consumes || function(chunkId, promises) {},
__webpack_require__.f.remotes || function(chunkId, promises) {},
// since call handler for this chunk
].reduce((p, handler) => (handler('pages/_document', p), p), promises)
// use require.x which already calls require.f for all other initialChunks
).then(() => (__webpack_require__.X(0, ["vendor-chunks/next"], () => (__webpack_exec__("./pages/_document.js")))));
module.exports = __webpack_exports__;In RemoteRuntimeModule and ConsumeSharedRuntimeModule, we should getAllReferencedChunks - this ensures that initialChunks or custom splitChunks can be tracked by federation chunk handlers, this also allows splitChunks to work again on more than splitChunks: async
const allChunks = [...(this.chunk?.getAllReferencedChunks() || [])];
for (const chunk of allChunks) {The remoteEntry should not be wrapped in require.X as this will convert it to promise when remote global should return get,init
Are you willing to work on this yourself?
yes
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Start date
Target date
Effort
Activity
Neerajpathak07 commentedon Oct 1, 2024
would like to work on this ,
Will be sending a pr by the weekend
ScriptedAlchemy commentedon Oct 1, 2024
@Neerajpathak07 you can look at how i implemented them in my repo.
https://github.com/module-federation/core/tree/main/packages/enhanced/src/lib/startup
See where these plugins are applied in my codebase
Neerajpathak07 commentedon Oct 2, 2024
Sure, I'll Look into it.
Neerajpathak07 commentedon Oct 3, 2024
@ScriptedAlchemy should I create a file and add the Plugin for mfStartupChunkDependenciesPlugin as well?
ScriptedAlchemy commentedon Oct 4, 2024
There was already a startup dependencies plugin in webpack. Ideally we just add some logic like mine into that. I think. You can always pr a new plugin and see what review says
Neerajpathak07 commentedon Oct 4, 2024
Ok Got it !
ScriptedAlchemy commentedon Oct 6, 2024
This depends on a PR if have open i think. The hoist container reference stuff. But that is to fix a problem with eager modules. The entry either way will need async wrapper
Neerajpathak07 commentedon Oct 8, 2024
Oh! I guess that's the reason The entry point is treated as an async chunk, and its dependencies are loaded before execution.
ScriptedAlchemy commentedon Oct 8, 2024
Yes. But hoist refs only fixes single runtime chunk issues. Async with main.js etc won't need that
Neerajpathak07 commentedon Oct 9, 2024
Oh! now I get it.
ScriptedAlchemy commentedon Oct 10, 2024
The idea is to move the async boundary out of user code and just change how an entrypoint requires its initial module. So it will only trigger after we check all requirements are met. Try applying this to a normal build with just a min entry. Then share react. Without these mods it will throw eager consumption error. With them it will not.
ScriptedAlchemy commentedon Oct 10, 2024
These changes make an asynchronous startup. So main.js exports a promise that resolves the inner require. Entry acts the same as if it were import().
I changed from passive mode to active. Which usually is for ESM async entry. So I use startup entrypoint runtime requirement.
That's require.x for its chunks and split chunks. Then the code I add to startup helpers checks if the chunk has federation-entry-startup or something as a runtime requirement. If so, I think I call the remotes and consumes handlers myself for the entry chunk ID, like main.
Require.x will call the handler for all else like dependOn, etc. But there needs to be an extra wrap to also ensure the main chunk.
Neerajpathak07 commentedon Oct 11, 2024
Oh! alright then I'll add an extra wrap for the main chunk are send a pr again.
ScriptedAlchemy commentedon Oct 11, 2024
If you have any questions you can dm me on Twitter. Also my core repo under module federation has this fully implemented if you want to see the difference etc
Neerajpathak07 commentedon Oct 12, 2024
Yeah Sure I'll refer to that as well
issue:-webpack#18812:- Removed the need for Async Import
Neerajpathak07 commentedon Oct 15, 2024
@ScriptedAlchemy
I have created a Pr.
pls review it.
feat: Removing-async-import for webpack#18812
# issue - 18812
pranjalisr commentedon Mar 22, 2025
Implement the below code in
StartupChunkDependenciesPluginor a similar location.compilation.hooks.additionalChunkRuntimeRequirements.tap(
'MyStartupChunkDependenciesPlugin',
(chunk, set, { chunkGraph }) => {
if (!isEnabledForChunk(chunk)) return;
if (chunkGraph.getNumberOfEntryModules(chunk) === 0) return;
set.add(federationStartup);
}
);
It might help
LeonxLJX commentedon Sep 2, 2026
I'd like to take this one.
I've read through the report. Before changing anything I'd like to reproduce it against current
mainfirst — my reasoning is that a fix anchored to the actual observed behavior is much easier to review than one inferred from the report alone, especially if the surrounding code has moved since this was filed.Plan: reproduce and confirm the root cause, then make the smallest change that addresses it (plus a regression test if the repo has coverage for this area). If the root cause turns out to live somewhere other than where the report points, I'll report back here before forcing a change.
Anything already known about this that I should read first?
(claiming via @LeonxLJX)