/* MIT License http://www.opensource.org/licenses/mit-license.php Author Zackary Jackson @ScriptedAlchemy */ "use strict"; const AsyncDependenciesBlock = require("../AsyncDependenciesBlock"); const ExternalModule = require("../ExternalModule"); const { STAGE_ADVANCED } = require("../OptimizationStages"); const memoize = require("../util/memoize"); const { forEachRuntime } = require("../util/runtime"); /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../Module")} Module */ const getModuleFederationPlugin = memoize(() => require("./ModuleFederationPlugin") ); const PLUGIN_NAME = "HoistContainerReferences"; /** * This class is used to hoist container references in the code. */ class HoistContainerReferences { /** * Apply the plugin to the compiler. * @param {Compiler} compiler The webpack compiler instance. */ apply(compiler) { compiler.hooks.thisCompilation.tap(PLUGIN_NAME, compilation => { const hooks = getModuleFederationPlugin().getCompilationHooks(compilation); const depsToTrace = new Set(); const entryExternalsToHoist = new Set(); hooks.addContainerEntryDependency.tap(PLUGIN_NAME, dep => { depsToTrace.add(dep); }); hooks.addFederationRuntimeDependency.tap(PLUGIN_NAME, dep => { depsToTrace.add(dep); }); compilation.hooks.addEntry.tap(PLUGIN_NAME, entryDep => { if (entryDep.type === "entry") { entryExternalsToHoist.add(entryDep); } }); // Hook into the optimizeChunks phase compilation.hooks.optimizeChunks.tap( { name: PLUGIN_NAME, // advanced stage is where SplitChunksPlugin runs. stage: STAGE_ADVANCED + 1 }, chunks => { this.hoistModulesInChunks( compilation, depsToTrace, entryExternalsToHoist ); } ); }); } /** * Hoist modules in chunks. * @param {Compilation} compilation The webpack compilation instance. * @param {Set} depsToTrace Set of container entry dependencies. * @param {Set} entryExternalsToHoist Set of container entry dependencies to hoist. */ hoistModulesInChunks(compilation, depsToTrace, entryExternalsToHoist) { const { chunkGraph, moduleGraph } = compilation; // loop over entry points for (const dep of entryExternalsToHoist) { const entryModule = moduleGraph.getModule(dep); if (!entryModule) continue; // get all the external module types and hoist them to the runtime chunk, this will get RemoteModule externals const allReferencedModules = getAllReferencedModules( compilation, entryModule, "external", false ); const containerRuntimes = chunkGraph.getModuleRuntimes(entryModule); const runtimes = new Set(); for (const runtimeSpec of containerRuntimes) { 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 allReferencedModules) { if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) { chunkGraph.connectChunkAndModule(runtimeChunk, module); } } } this.cleanUpChunks(compilation, allReferencedModules); } // handle container entry specifically for (const dep of depsToTrace) { const containerEntryModule = moduleGraph.getModule(dep); if (!containerEntryModule) continue; const allReferencedModules = getAllReferencedModules( compilation, containerEntryModule, "initial", false ); const allRemoteReferences = getAllReferencedModules( compilation, containerEntryModule, "external", false ); for (const remote of allRemoteReferences) { allReferencedModules.add(remote); } const containerRuntimes = chunkGraph.getModuleRuntimes(containerEntryModule); const runtimes = new Set(); for (const runtimeSpec of containerRuntimes) { 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 allReferencedModules) { if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) { chunkGraph.connectChunkAndModule(runtimeChunk, module); } } } this.cleanUpChunks(compilation, allReferencedModules); } } /** * Clean up chunks by disconnecting unused modules. * @param {Compilation} compilation The webpack compilation instance. * @param {Set} modules Set of modules to clean up. */ cleanUpChunks(compilation, modules) { const { chunkGraph } = compilation; for (const module of modules) { 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 collect all referenced modules recursively. * @param {Compilation} compilation The webpack compilation instance. * @param {Module} module The module to start collecting from. * @param {string} type The type of modules to collect ("initial", "external", or "all"). * @param {boolean} includeInitial Should include the referenced module passed * @returns {Set} Set of collected modules. */ function getAllReferencedModules(compilation, module, type, includeInitial) { const collectedModules = new Set(includeInitial ? [module] : []); const visitedModules = new WeakSet([module]); const stack = [module]; while (stack.length > 0) { const currentModule = stack.pop(); if (!currentModule) continue; const outgoingConnections = compilation.moduleGraph.getOutgoingConnections(currentModule); if (outgoingConnections) { for (const connection of outgoingConnections) { const connectedModule = connection.module; // Skip if module has already been visited if (!connectedModule || visitedModules.has(connectedModule)) { continue; } // Handle 'initial' type (skipping async blocks) if (type === "initial") { const parentBlock = compilation.moduleGraph.getParentBlock( /** @type {Dependency} */ (connection.dependency) ); if (parentBlock instanceof AsyncDependenciesBlock) { continue; } } // Handle 'external' type (collecting only external modules) if (type === "external") { if (connection.module instanceof ExternalModule) { collectedModules.add(connectedModule); } } else { // Handle 'all' or unspecified types collectedModules.add(connectedModule); } // Add connected module to the stack and mark it as visited visitedModules.add(connectedModule); stack.push(connectedModule); } } } return collectedModules; } module.exports = HoistContainerReferences;