mirror of https://github.com/webpack/webpack.git
feat: Single Runtime Chunk and Federation eager module hoisting
This commit is contained in:
commit
9903856854
|
@ -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();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import React from "react";
|
||||
import ComponentA from "containerA/ComponentA";
|
||||
|
||||
export default () => {
|
||||
return `App rendered with [${React()}] and [${ComponentA()}]`;
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
export default () => {
|
||||
return `ComponentA rendered with [${React()}]`;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
import('containerB/ComponentA')
|
|
@ -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]]"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
3
test/configCases/container/reference-hoisting/node_modules/react.js
generated
vendored
Normal file
3
test/configCases/container/reference-hoisting/node_modules/react.js
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
let version = "0.1.2";
|
||||
export default () => `This is react ${version}`;
|
||||
export function setVersion(v) { version = v; }
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
findBundle: function (i, options) {
|
||||
return i === 0 ? "./main.js" : "./module/main.mjs";
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { setVersion } from "react";
|
||||
|
||||
export default function upgrade() {
|
||||
setVersion("1.2.3");
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
];
|
|
@ -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 {
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue