Skip to content

Remove Need for Async Import in Module Federation  #18812

@ScriptedAlchemy

Description

@ScriptedAlchemy

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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions