| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Zackary Jackson @ScriptedAlchemy
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
|
|---|
| 9 | const ExternalModule = require("../ExternalModule");
|
|---|
| 10 | const { STAGE_ADVANCED } = require("../OptimizationStages");
|
|---|
| 11 | const memoize = require("../util/memoize");
|
|---|
| 12 | const { forEachRuntime } = require("../util/runtime");
|
|---|
| 13 |
|
|---|
| 14 | /** @typedef {import("../Compilation")} Compilation */
|
|---|
| 15 | /** @typedef {import("../Compiler")} Compiler */
|
|---|
| 16 | /** @typedef {import("../Dependency")} Dependency */
|
|---|
| 17 | /** @typedef {import("../Module")} Module */
|
|---|
| 18 |
|
|---|
| 19 | const getModuleFederationPlugin = memoize(() =>
|
|---|
| 20 | require("./ModuleFederationPlugin")
|
|---|
| 21 | );
|
|---|
| 22 |
|
|---|
| 23 | const PLUGIN_NAME = "HoistContainerReferences";
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * This class is used to hoist container references in the code.
|
|---|
| 27 | */
|
|---|
| 28 | class HoistContainerReferences {
|
|---|
| 29 | /**
|
|---|
| 30 | * Apply the plugin to the compiler.
|
|---|
| 31 | * @param {Compiler} compiler The webpack compiler instance.
|
|---|
| 32 | */
|
|---|
| 33 | apply(compiler) {
|
|---|
| 34 | compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|---|
| 35 | const hooks =
|
|---|
| 36 | getModuleFederationPlugin().getCompilationHooks(compilation);
|
|---|
| 37 | /** @type {Set<Dependency>} */
|
|---|
| 38 | const depsToTrace = new Set();
|
|---|
| 39 | /** @type {Set<Dependency>} */
|
|---|
| 40 | const entryExternalsToHoist = new Set();
|
|---|
| 41 | hooks.addContainerEntryDependency.tap(PLUGIN_NAME, (dep) => {
|
|---|
| 42 | depsToTrace.add(dep);
|
|---|
| 43 | });
|
|---|
| 44 | hooks.addFederationRuntimeDependency.tap(PLUGIN_NAME, (dep) => {
|
|---|
| 45 | depsToTrace.add(dep);
|
|---|
| 46 | });
|
|---|
| 47 |
|
|---|
| 48 | compilation.hooks.addEntry.tap(PLUGIN_NAME, (entryDep) => {
|
|---|
| 49 | if (entryDep.type === "entry") {
|
|---|
| 50 | entryExternalsToHoist.add(entryDep);
|
|---|
| 51 | }
|
|---|
| 52 | });
|
|---|
| 53 |
|
|---|
| 54 | // Hook into the optimizeChunks phase
|
|---|
| 55 | compilation.hooks.optimizeChunks.tap(
|
|---|
| 56 | {
|
|---|
| 57 | name: PLUGIN_NAME,
|
|---|
| 58 | // advanced stage is where SplitChunksPlugin runs.
|
|---|
| 59 | stage: STAGE_ADVANCED + 1
|
|---|
| 60 | },
|
|---|
| 61 | (_chunks) => {
|
|---|
| 62 | this.hoistModulesInChunks(
|
|---|
| 63 | compilation,
|
|---|
| 64 | depsToTrace,
|
|---|
| 65 | entryExternalsToHoist
|
|---|
| 66 | );
|
|---|
| 67 | }
|
|---|
| 68 | );
|
|---|
| 69 | });
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 | /**
|
|---|
| 73 | * Hoist modules in chunks.
|
|---|
| 74 | * @param {Compilation} compilation The webpack compilation instance.
|
|---|
| 75 | * @param {Set<Dependency>} depsToTrace Set of container entry dependencies.
|
|---|
| 76 | * @param {Set<Dependency>} entryExternalsToHoist Set of container entry dependencies to hoist.
|
|---|
| 77 | */
|
|---|
| 78 | hoistModulesInChunks(compilation, depsToTrace, entryExternalsToHoist) {
|
|---|
| 79 | const { chunkGraph, moduleGraph } = compilation;
|
|---|
| 80 |
|
|---|
| 81 | // loop over entry points
|
|---|
| 82 | for (const dep of entryExternalsToHoist) {
|
|---|
| 83 | const entryModule = moduleGraph.getModule(dep);
|
|---|
| 84 | if (!entryModule) continue;
|
|---|
| 85 | // get all the external module types and hoist them to the runtime chunk, this will get RemoteModule externals
|
|---|
| 86 | const allReferencedModules = getAllReferencedModules(
|
|---|
| 87 | compilation,
|
|---|
| 88 | entryModule,
|
|---|
| 89 | "external",
|
|---|
| 90 | false
|
|---|
| 91 | );
|
|---|
| 92 |
|
|---|
| 93 | const containerRuntimes = chunkGraph.getModuleRuntimes(entryModule);
|
|---|
| 94 | /** @type {Set<string>} */
|
|---|
| 95 | const runtimes = new Set();
|
|---|
| 96 |
|
|---|
| 97 | for (const runtimeSpec of containerRuntimes) {
|
|---|
| 98 | forEachRuntime(runtimeSpec, (runtimeKey) => {
|
|---|
| 99 | if (runtimeKey) {
|
|---|
| 100 | runtimes.add(runtimeKey);
|
|---|
| 101 | }
|
|---|
| 102 | });
|
|---|
| 103 | }
|
|---|
| 104 |
|
|---|
| 105 | for (const runtime of runtimes) {
|
|---|
| 106 | const runtimeChunk = compilation.namedChunks.get(runtime);
|
|---|
| 107 | if (!runtimeChunk) continue;
|
|---|
| 108 |
|
|---|
| 109 | for (const module of allReferencedModules) {
|
|---|
| 110 | if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
|
|---|
| 111 | chunkGraph.connectChunkAndModule(runtimeChunk, module);
|
|---|
| 112 | }
|
|---|
| 113 | }
|
|---|
| 114 | }
|
|---|
| 115 | this.cleanUpChunks(compilation, allReferencedModules);
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | // handle container entry specifically
|
|---|
| 119 | for (const dep of depsToTrace) {
|
|---|
| 120 | const containerEntryModule = moduleGraph.getModule(dep);
|
|---|
| 121 | if (!containerEntryModule) continue;
|
|---|
| 122 | const allReferencedModules = getAllReferencedModules(
|
|---|
| 123 | compilation,
|
|---|
| 124 | containerEntryModule,
|
|---|
| 125 | "initial",
|
|---|
| 126 | false
|
|---|
| 127 | );
|
|---|
| 128 |
|
|---|
| 129 | const allRemoteReferences = getAllReferencedModules(
|
|---|
| 130 | compilation,
|
|---|
| 131 | containerEntryModule,
|
|---|
| 132 | "external",
|
|---|
| 133 | false
|
|---|
| 134 | );
|
|---|
| 135 |
|
|---|
| 136 | for (const remote of allRemoteReferences) {
|
|---|
| 137 | allReferencedModules.add(remote);
|
|---|
| 138 | }
|
|---|
| 139 |
|
|---|
| 140 | const containerRuntimes =
|
|---|
| 141 | chunkGraph.getModuleRuntimes(containerEntryModule);
|
|---|
| 142 | /** @type {Set<string>} */
|
|---|
| 143 | const runtimes = new Set();
|
|---|
| 144 |
|
|---|
| 145 | for (const runtimeSpec of containerRuntimes) {
|
|---|
| 146 | forEachRuntime(runtimeSpec, (runtimeKey) => {
|
|---|
| 147 | if (runtimeKey) {
|
|---|
| 148 | runtimes.add(runtimeKey);
|
|---|
| 149 | }
|
|---|
| 150 | });
|
|---|
| 151 | }
|
|---|
| 152 |
|
|---|
| 153 | for (const runtime of runtimes) {
|
|---|
| 154 | const runtimeChunk = compilation.namedChunks.get(runtime);
|
|---|
| 155 | if (!runtimeChunk) continue;
|
|---|
| 156 |
|
|---|
| 157 | for (const module of allReferencedModules) {
|
|---|
| 158 | if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) {
|
|---|
| 159 | chunkGraph.connectChunkAndModule(runtimeChunk, module);
|
|---|
| 160 | }
|
|---|
| 161 | }
|
|---|
| 162 | }
|
|---|
| 163 | this.cleanUpChunks(compilation, allReferencedModules);
|
|---|
| 164 | }
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | /**
|
|---|
| 168 | * Clean up chunks by disconnecting unused modules.
|
|---|
| 169 | * @param {Compilation} compilation The webpack compilation instance.
|
|---|
| 170 | * @param {Set<Module>} modules Set of modules to clean up.
|
|---|
| 171 | */
|
|---|
| 172 | cleanUpChunks(compilation, modules) {
|
|---|
| 173 | const { chunkGraph } = compilation;
|
|---|
| 174 | for (const module of modules) {
|
|---|
| 175 | for (const chunk of chunkGraph.getModuleChunks(module)) {
|
|---|
| 176 | if (!chunk.hasRuntime()) {
|
|---|
| 177 | chunkGraph.disconnectChunkAndModule(chunk, module);
|
|---|
| 178 | if (
|
|---|
| 179 | chunkGraph.getNumberOfChunkModules(chunk) === 0 &&
|
|---|
| 180 | chunkGraph.getNumberOfEntryModules(chunk) === 0
|
|---|
| 181 | ) {
|
|---|
| 182 | chunkGraph.disconnectChunk(chunk);
|
|---|
| 183 | compilation.chunks.delete(chunk);
|
|---|
| 184 | if (chunk.name) {
|
|---|
| 185 | compilation.namedChunks.delete(chunk.name);
|
|---|
| 186 | }
|
|---|
| 187 | }
|
|---|
| 188 | }
|
|---|
| 189 | }
|
|---|
| 190 | }
|
|---|
| 191 | modules.clear();
|
|---|
| 192 | }
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | /**
|
|---|
| 196 | * Helper method to collect all referenced modules recursively.
|
|---|
| 197 | * @param {Compilation} compilation The webpack compilation instance.
|
|---|
| 198 | * @param {Module} module The module to start collecting from.
|
|---|
| 199 | * @param {string} type The type of modules to collect ("initial", "external", or "all").
|
|---|
| 200 | * @param {boolean} includeInitial Should include the referenced module passed
|
|---|
| 201 | * @returns {Set<Module>} Set of collected modules.
|
|---|
| 202 | */
|
|---|
| 203 | function getAllReferencedModules(compilation, module, type, includeInitial) {
|
|---|
| 204 | const collectedModules = new Set(includeInitial ? [module] : []);
|
|---|
| 205 | /** @type {WeakSet<Module>} */
|
|---|
| 206 | const visitedModules = new WeakSet([module]);
|
|---|
| 207 | /** @type {Module[]} */
|
|---|
| 208 | const stack = [module];
|
|---|
| 209 |
|
|---|
| 210 | while (stack.length > 0) {
|
|---|
| 211 | const currentModule = stack.pop();
|
|---|
| 212 | if (!currentModule) continue;
|
|---|
| 213 |
|
|---|
| 214 | const outgoingConnections =
|
|---|
| 215 | compilation.moduleGraph.getOutgoingConnections(currentModule);
|
|---|
| 216 | if (outgoingConnections) {
|
|---|
| 217 | for (const connection of outgoingConnections) {
|
|---|
| 218 | const connectedModule = connection.module;
|
|---|
| 219 |
|
|---|
| 220 | // Skip if module has already been visited
|
|---|
| 221 | if (!connectedModule || visitedModules.has(connectedModule)) {
|
|---|
| 222 | continue;
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | // Handle 'initial' type (skipping async blocks)
|
|---|
| 226 | if (type === "initial") {
|
|---|
| 227 | const parentBlock = compilation.moduleGraph.getParentBlock(
|
|---|
| 228 | /** @type {Dependency} */
|
|---|
| 229 | (connection.dependency)
|
|---|
| 230 | );
|
|---|
| 231 | if (parentBlock instanceof AsyncDependenciesBlock) {
|
|---|
| 232 | continue;
|
|---|
| 233 | }
|
|---|
| 234 | }
|
|---|
| 235 |
|
|---|
| 236 | // Handle 'external' type (collecting only external modules)
|
|---|
| 237 | if (type === "external") {
|
|---|
| 238 | if (connection.module instanceof ExternalModule) {
|
|---|
| 239 | collectedModules.add(connectedModule);
|
|---|
| 240 | }
|
|---|
| 241 | } else {
|
|---|
| 242 | // Handle 'all' or unspecified types
|
|---|
| 243 | collectedModules.add(connectedModule);
|
|---|
| 244 | }
|
|---|
| 245 |
|
|---|
| 246 | // Add connected module to the stack and mark it as visited
|
|---|
| 247 | visitedModules.add(connectedModule);
|
|---|
| 248 | stack.push(connectedModule);
|
|---|
| 249 | }
|
|---|
| 250 | }
|
|---|
| 251 | }
|
|---|
| 252 |
|
|---|
| 253 | return collectedModules;
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | module.exports = HoistContainerReferences;
|
|---|