Skip to content

feat: enhance HoistContainerReferencesPlugin for better module hoisting #3903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: refactor/hook-renaming-cleanup-v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/enhanced-hoist-container-refs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@module-federation/enhanced": patch
---

feat: enhance HoistContainerReferencesPlugin for better module hoisting

- Separate handling for container, federation, and remote dependencies
- Improved support for `runtimeChunk: 'single'` configuration
- Proper remote module hoisting using the new `addRemoteDependency` hook
- Simplified cleanup logic for better performance
- Changed runtime chunk detection to include all chunks with runtime (not just entry chunks)
- Added comprehensive unit tests for the plugin functionality
135 changes: 93 additions & 42 deletions packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {
Chunk,
Compiler,
Compilation,
Chunk,
WebpackPluginInstance,
Module,
Dependency,
Expand All @@ -10,6 +10,8 @@ import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-p
import FederationModulesPlugin from './runtime/FederationModulesPlugin';
import ContainerEntryDependency from './ContainerEntryDependency';
import FederationRuntimeDependency from './runtime/FederationRuntimeDependency';
import RemoteToExternalDependency from './RemoteToExternalDependency';
import FallbackDependency from './FallbackDependency';

const { AsyncDependenciesBlock, ExternalModule } = require(
normalizeWebpackPath('webpack'),
Expand All @@ -18,16 +20,19 @@ const { AsyncDependenciesBlock, ExternalModule } = require(
const PLUGIN_NAME = 'HoistContainerReferences';

/**
* This class is used to hoist container references in the code.
* This plugin hoists container-related modules into runtime chunks when using runtimeChunk: single configuration.
*/
export class HoistContainerReferences implements WebpackPluginInstance {
class HoistContainerReferences implements WebpackPluginInstance {
apply(compiler: Compiler): void {
compiler.hooks.thisCompilation.tap(
PLUGIN_NAME,
(compilation: Compilation) => {
const logger = compilation.getLogger(PLUGIN_NAME);
const hooks = FederationModulesPlugin.getCompilationHooks(compilation);
const containerEntryDependencies = new Set<Dependency>();
const federationRuntimeDependencies = new Set<Dependency>();
const remoteDependencies = new Set<Dependency>();

hooks.addContainerEntryDependency.tap(
'HoistContainerReferences',
(dep: ContainerEntryDependency) => {
Expand All @@ -37,7 +42,13 @@ export class HoistContainerReferences implements WebpackPluginInstance {
hooks.addFederationRuntimeDependency.tap(
'HoistContainerReferences',
(dep: FederationRuntimeDependency) => {
containerEntryDependencies.add(dep);
federationRuntimeDependencies.add(dep);
},
);
hooks.addRemoteDependency.tap(
'HoistContainerReferences',
(dep: RemoteToExternalDependency | FallbackDependency) => {
remoteDependencies.add(dep);
},
);

Expand All @@ -53,9 +64,10 @@ export class HoistContainerReferences implements WebpackPluginInstance {
this.hoistModulesInChunks(
compilation,
runtimeChunks,
chunks,
logger,
containerEntryDependencies,
federationRuntimeDependencies,
remoteDependencies,
);
},
);
Expand All @@ -67,37 +79,60 @@ export class HoistContainerReferences implements WebpackPluginInstance {
private hoistModulesInChunks(
compilation: Compilation,
runtimeChunks: Set<Chunk>,
chunks: Iterable<Chunk>,
logger: ReturnType<Compilation['getLogger']>,
containerEntryDependencies: Set<Dependency>,
federationRuntimeDependencies: Set<Dependency>,
remoteDependencies: Set<Dependency>,
): void {
const { chunkGraph, moduleGraph } = compilation;
// when runtimeChunk: single is set - ContainerPlugin will create a "partial" chunk we can use to
// move modules into the runtime chunk
const allModulesToHoist = new Set<Module>();

// Process container entry dependencies (needed for nextjs-mf exposed modules)
for (const dep of containerEntryDependencies) {
const containerEntryModule = moduleGraph.getModule(dep);
if (!containerEntryModule) continue;
const allReferencedModules = getAllReferencedModules(
const referencedModules = getAllReferencedModules(
compilation,
containerEntryModule,
'initial',
);
referencedModules.forEach((m: Module) => allModulesToHoist.add(m));
const moduleRuntimes = chunkGraph.getModuleRuntimes(containerEntryModule);
const runtimes = new Set<string>();
for (const runtimeSpec of moduleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
if (runtimeKey) {
runtimes.add(runtimeKey);
}
},
);
}
for (const runtime of runtimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
}
}
}
}

const allRemoteReferences = getAllReferencedModules(
// Federation Runtime Dependencies: use 'initial' (not 'all')
for (const dep of federationRuntimeDependencies) {
const runtimeModule = moduleGraph.getModule(dep);
if (!runtimeModule) continue;
const referencedModules = getAllReferencedModules(
compilation,
containerEntryModule,
'external',
runtimeModule,
'initial',
);

for (const remote of allRemoteReferences) {
allReferencedModules.add(remote);
}

const containerRuntimes =
chunkGraph.getModuleRuntimes(containerEntryModule);
referencedModules.forEach((m: Module) => allModulesToHoist.add(m));
const moduleRuntimes = chunkGraph.getModuleRuntimes(runtimeModule);
const runtimes = new Set<string>();

for (const runtimeSpec of containerRuntimes) {
for (const runtimeSpec of moduleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
Expand All @@ -107,19 +142,49 @@ export class HoistContainerReferences implements WebpackPluginInstance {
},
);
}

for (const runtime of runtimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
}
}
}
}

for (const module of allReferencedModules) {
// Process remote dependencies
for (const remoteDep of remoteDependencies) {
const remoteModule = moduleGraph.getModule(remoteDep);
if (!remoteModule) continue;
const referencedRemoteModules = getAllReferencedModules(
compilation,
remoteModule,
'initial',
);
referencedRemoteModules.forEach((m: Module) => allModulesToHoist.add(m));
const remoteModuleRuntimes = chunkGraph.getModuleRuntimes(remoteModule);
const remoteRuntimes = new Set<string>();
for (const runtimeSpec of remoteModuleRuntimes) {
compilation.compiler.webpack.util.runtime.forEachRuntime(
runtimeSpec,
(runtimeKey) => {
if (runtimeKey) remoteRuntimes.add(runtimeKey);
},
);
}
for (const runtime of remoteRuntimes) {
const runtimeChunk = compilation.namedChunks.get(runtime);
if (!runtimeChunk) continue;
for (const module of referencedRemoteModules) {
if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
chunkGraph.connectChunkAndModule(runtimeChunk, module);
}
}
}
this.cleanUpChunks(compilation, allReferencedModules);
}

this.cleanUpChunks(compilation, allModulesToHoist);
}

// Method to clean up chunks by disconnecting unused modules
Expand All @@ -129,31 +194,17 @@ export class HoistContainerReferences implements WebpackPluginInstance {
for (const chunk of chunkGraph.getModuleChunks(module)) {
if (!chunk.hasRuntime()) {
chunkGraph.disconnectChunkAndModule(chunk, module);
if (
chunkGraph.getNumberOfChunkModules(chunk) === 0 &&
chunkGraph.getNumberOfEntryModules(chunk) === 0
) {
chunkGraph.disconnectChunk(chunk);
compilation.chunks.delete(chunk);
if (chunk.name) {
compilation.namedChunks.delete(chunk.name);
}
}
}
}
}
modules.clear();
}

// Helper method to get runtime chunks from the compilation
// Method to get runtime chunks
private getRuntimeChunks(compilation: Compilation): Set<Chunk> {
const runtimeChunks = new Set<Chunk>();
const entries = compilation.entrypoints;

for (const entrypoint of entries.values()) {
const runtimeChunk = entrypoint.getRuntimeChunk();
if (runtimeChunk) {
runtimeChunks.add(runtimeChunk);
for (const chunk of compilation.chunks) {
if (chunk.hasRuntime()) {
runtimeChunks.add(chunk);
}
}
return runtimeChunks;
Expand Down
Loading