-
-
Notifications
You must be signed in to change notification settings - Fork 9.2k
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