feat: Single Runtime Chunk and Federation eager module hoisting

This commit is contained in:
Alexander Akait 2024-10-21 19:18:14 +03:00 committed by GitHub
commit 9903856854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 430 additions and 0 deletions

View File

@ -6,6 +6,7 @@
"use strict";
const createSchemaValidation = require("../util/create-schema-validation");
const memoize = require("../util/memoize");
const ContainerEntryDependency = require("./ContainerEntryDependency");
const ContainerEntryModuleFactory = require("./ContainerEntryModuleFactory");
const ContainerExposedDependency = require("./ContainerExposedDependency");
@ -16,6 +17,10 @@ const { parseOptions } = require("./options");
/** @typedef {import("./ContainerEntryModule").ExposeOptions} ExposeOptions */
/** @typedef {import("./ContainerEntryModule").ExposesList} ExposesList */
const getModuleFederationPlugin = memoize(() =>
require("./ModuleFederationPlugin")
);
const validate = createSchemaValidation(
require("../../schemas/plugins/container/ContainerPlugin.check.js"),
() => require("../../schemas/plugins/container/ContainerPlugin.json"),
@ -73,6 +78,8 @@ class ContainerPlugin {
}
compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => {
const hooks =
getModuleFederationPlugin().getCompilationHooks(compilation);
const dep = new ContainerEntryDependency(name, exposes, shareScope);
dep.loc = { name };
compilation.addEntry(
@ -86,6 +93,7 @@ class ContainerPlugin {
},
error => {
if (error) return callback(error);
hooks.addContainerEntryDependency.call(dep);
callback();
}
);

View File

@ -0,0 +1,244 @@
/*
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");
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 {import("../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 {import("../Compilation")} compilation The webpack compilation instance.
* @param {Set<import("../Dependency")>} depsToTrace Set of container entry dependencies.
* @param {Set<import("../Dependency")>} 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 {import("../Compilation")} compilation The webpack compilation instance.
* @param {Set<import("../Module")>} 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 {import("../Compilation")} compilation The webpack compilation instance.
* @param {import("../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<import("../Module")>} 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(
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;

View File

@ -5,17 +5,26 @@
"use strict";
const { SyncHook } = require("tapable");
const isValidExternalsType = require("../../schemas/plugins/container/ExternalsType.check.js");
const Compilation = require("../Compilation");
const SharePlugin = require("../sharing/SharePlugin");
const createSchemaValidation = require("../util/create-schema-validation");
const ContainerPlugin = require("./ContainerPlugin");
const ContainerReferencePlugin = require("./ContainerReferencePlugin");
const HoistContainerReferences = require("./HoistContainerReferencesPlugin");
/** @typedef {import("../../declarations/plugins/container/ModuleFederationPlugin").ExternalsType} ExternalsType */
/** @typedef {import("../../declarations/plugins/container/ModuleFederationPlugin").ModuleFederationPluginOptions} ModuleFederationPluginOptions */
/** @typedef {import("../../declarations/plugins/container/ModuleFederationPlugin").Shared} Shared */
/** @typedef {import("../Compiler")} Compiler */
/**
* @typedef {object} CompilationHooks
* @property {SyncHook} addContainerEntryDependency
* @property {SyncHook} addFederationRuntimeDependency
*/
const validate = createSchemaValidation(
require("../../schemas/plugins/container/ModuleFederationPlugin.check.js"),
() => require("../../schemas/plugins/container/ModuleFederationPlugin.json"),
@ -24,6 +33,10 @@ const validate = createSchemaValidation(
baseDataPath: "options"
}
);
/** @type {WeakMap<Compilation, CompilationHooks>} */
const compilationHooksMap = new WeakMap();
class ModuleFederationPlugin {
/**
* @param {ModuleFederationPluginOptions} options options
@ -34,6 +47,28 @@ class ModuleFederationPlugin {
this._options = options;
}
/**
* Get the compilation hooks associated with this plugin.
* @param {Compilation} compilation The compilation instance.
* @returns {CompilationHooks} The hooks for the compilation.
*/
static getCompilationHooks(compilation) {
if (!(compilation instanceof Compilation)) {
throw new TypeError(
"The 'compilation' argument must be an instance of Compilation"
);
}
let hooks = compilationHooksMap.get(compilation);
if (!hooks) {
hooks = {
addContainerEntryDependency: new SyncHook(["dependency"]),
addFederationRuntimeDependency: new SyncHook(["dependency"])
};
compilationHooksMap.set(compilation, hooks);
}
return hooks;
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
@ -87,6 +122,7 @@ class ModuleFederationPlugin {
shareScope: options.shareScope
}).apply(compiler);
}
new HoistContainerReferences().apply(compiler);
});
}
}

View File

@ -0,0 +1,6 @@
import React from "react";
import ComponentA from "containerA/ComponentA";
export default () => {
return `App rendered with [${React()}] and [${ComponentA()}]`;
};

View File

@ -0,0 +1,5 @@
import React from "react";
export default () => {
return `ComponentA rendered with [${React()}]`;
};

View File

@ -0,0 +1 @@
import('containerB/ComponentA')

View File

@ -0,0 +1,21 @@
it("should have the hoisted container references", () => {
const wpm = __webpack_modules__;
expect(wpm).toHaveProperty("webpack/container/reference/containerA");
expect(wpm).toHaveProperty("webpack/container/reference/containerB");
});
it("should load the component from container", () => {
return import("./App").then(({ default: App }) => {
const rendered = App();
expect(rendered).toBe(
"App rendered with [This is react 0.1.2] and [ComponentA rendered with [This is react 0.1.2]]"
);
return import("./upgrade-react").then(({ default: upgrade }) => {
upgrade();
const rendered = App();
expect(rendered).toBe(
"App rendered with [This is react 1.2.3] and [ComponentA rendered with [This is react 1.2.3]]"
);
});
});
});

View File

@ -0,0 +1,3 @@
let version = "0.1.2";
export default () => `This is react ${version}`;
export function setVersion(v) { version = v; }

View File

@ -0,0 +1,5 @@
module.exports = {
findBundle: function (i, options) {
return i === 0 ? "./main.js" : "./module/main.mjs";
}
};

View File

@ -0,0 +1,5 @@
import { setVersion } from "react";
export default function upgrade() {
setVersion("1.2.3");
}

View File

@ -0,0 +1,85 @@
const { ModuleFederationPlugin } = require("../../../../").container;
/** @type {ConstructorParameters<typeof ModuleFederationPlugin>[0]} */
const common = {
name: "container",
exposes: {
"./ComponentA": {
import: "./ComponentA"
}
},
shared: {
react: {
version: false,
requiredVersion: false
}
}
};
/** @type {import("../../../../").Configuration[]} */
module.exports = [
{
entry: {
main: "./index.js",
other: "./index-2.js"
},
output: {
filename: "[name].js",
uniqueName: "ref-hoist"
},
optimization: {
runtimeChunk: "single",
moduleIds: "named"
},
plugins: [
new ModuleFederationPlugin({
runtime: false,
library: { type: "commonjs-module" },
filename: "container.js",
remotes: {
containerA: {
external: "./container.js"
},
containerB: {
external: "../0-container-full/container.js"
}
},
...common
})
]
},
{
entry: {
main: "./index.js",
other: "./index-2.js"
},
experiments: {
outputModule: true
},
optimization: {
runtimeChunk: "single",
moduleIds: "named"
},
output: {
filename: "module/[name].mjs",
uniqueName: "ref-hoist-mjs"
},
plugins: [
new ModuleFederationPlugin({
runtime: false,
library: { type: "module" },
filename: "module/container.mjs",
remotes: {
containerA: {
external: "./container.mjs"
},
containerB: {
external: "../../0-container-full/module/container.mjs"
}
},
...common
})
],
target: "node14"
}
];

11
types.d.ts vendored
View File

@ -2306,6 +2306,10 @@ declare interface CompilationHooksJavascriptModulesPlugin {
chunkHash: SyncHook<[Chunk, Hash, ChunkHashContext]>;
useSourceMap: SyncBailHook<[Chunk, RenderContext], boolean>;
}
declare interface CompilationHooksModuleFederationPlugin {
addContainerEntryDependency: SyncHook<any>;
addFederationRuntimeDependency: SyncHook<any>;
}
declare interface CompilationHooksRealContentHashPlugin {
updateHash: SyncBailHook<[Buffer[], string], string>;
}
@ -8478,6 +8482,13 @@ declare class ModuleFederationPlugin {
* Apply the plugin
*/
apply(compiler: Compiler): void;
/**
* Get the compilation hooks associated with this plugin.
*/
static getCompilationHooks(
compilation: Compilation
): CompilationHooksModuleFederationPlugin;
}
declare interface ModuleFederationPluginOptions {
/**