diff --git a/.changeset/ai-eager-cat.md b/.changeset/ai-eager-cat.md
new file mode 100644
index 0000000000..163b13d847
--- /dev/null
+++ b/.changeset/ai-eager-cat.md
@@ -0,0 +1,14 @@
+---
+"@module-federation/enhanced": minor
+---
+
+Introduce minimal runtime and experiment options for FederationRuntimePlugin.
+
+- Added support for minimal federation runtime dependency in `FederationRuntimeDependency` class.
+- Introduced `experiments` property to `EmbedFederationRuntimePlugin` class.
+- Enhanced `FederationRuntimePlugin` to support both standard and minimal runtime dependencies.
+ - Added logic to handle `useMinimalRuntime` option.
+ - Conditionally modified entry file path based on the runtime mode.
+- Adjusted constructor to initialize new experimental paths and dependencies.
+- Modified `getTemplate` and `getFilePath` methods to accommodate minimal runtime.
+- Updated `setRuntimeAlias` and `apply` methods to utilize new experiment options and embedded paths.
\ No newline at end of file
diff --git a/.changeset/ai-eager-tiger.md b/.changeset/ai-eager-tiger.md
new file mode 100644
index 0000000000..13277bd8a6
--- /dev/null
+++ b/.changeset/ai-eager-tiger.md
@@ -0,0 +1,11 @@
+---
+"@module-federation/runtime": patch
+---
+
+Refactored the script loading mechanism to use a more generalized loaderHook.
+
+- Replaced `createScriptHook` with `loaderHook` across various functions.
+ - Updated `loadEntryScript` function to use `loaderHook.lifecycle.createScript`.
+ - Modified `loadEntryDom` function to accept `loaderHook` instead of `createScriptHook`.
+ - Adjusted `loadEntryNode` function to handle `loaderHook.lifecycle.createScript`.
+- Streamlined the handling of script loading in `getRemoteEntry`.
\ No newline at end of file
diff --git a/.changeset/ai-gentle-eagle.md b/.changeset/ai-gentle-eagle.md
new file mode 100644
index 0000000000..29d241bd63
--- /dev/null
+++ b/.changeset/ai-gentle-eagle.md
@@ -0,0 +1,11 @@
+---
+"@module-federation/runtime": patch
+---
+
+Added support for defining and setting a shareable runtime globally to enhance modularity and reusability within the Federation system.
+
+- Defined a `ShareableRuntime` type encapsulating the core functionalities of the module federation.
+- Introduced `__SHAREABLE_RUNTIME__` to the `Federation` interface to store the `ShareableRuntime`.
+- Implemented `setGlobalShareableRuntime` function to set the shareable runtime if not already set.
+- Modified `FederationManager` methods (`preloadRemote`, `registerRemotes`, `registerPlugins`) to use the spread operator for cleaner code.
+- Initialized the global shareable runtime at the module's root with key components like `FederationManager`, `FederationHost`, etc.
diff --git a/.changeset/ai-noisy-fox.md b/.changeset/ai-noisy-fox.md
new file mode 100644
index 0000000000..1be2b76d60
--- /dev/null
+++ b/.changeset/ai-noisy-fox.md
@@ -0,0 +1,8 @@
+---
+"@module-federation/runtime": patch
+---
+
+- Added optional `bundlerId` parameter to FederationHost constructor.
+- Modified default logic to choose `bundlerId` if provided, otherwise fallback to `getBuilderId()`.
+- Updated `getGlobalFederationInstance` function to accept and utilize an optional `builderId`.
+- Ensured internal checks compare with the provided `bundlerId` for consistency in federation instances lookup.
diff --git a/.changeset/ai-sleepy-bear.md b/.changeset/ai-sleepy-bear.md
new file mode 100644
index 0000000000..b54cfec079
--- /dev/null
+++ b/.changeset/ai-sleepy-bear.md
@@ -0,0 +1,12 @@
+---
+"@module-federation/runtime": minor
+---
+
+Refactor initialization and management of Federation instances with the new FederationManager class.
+
+- Introduced FederationManager class to encapsulate federation management logic.
+ - FederationManager class now handles the initialization and operation methods.
+ - Methods `init`, `loadRemote`, `loadShare`, `loadShareSync`, `preloadRemote`, `registerRemotes`, and `registerPlugins` are now routed through an instance of FederationManager.
+- Updated test to exclude `FederationManager` from index.ts exports.
+- Minor code cleanup and added import for `getBuilderId` in `index.ts`.
+- Removed direct manipulation of a singleton FederationHost instance and replaced it with the FederationManager pattern.
\ No newline at end of file
diff --git a/.changeset/ai-sleepy-fox.md b/.changeset/ai-sleepy-fox.md
new file mode 100644
index 0000000000..cf97db0608
--- /dev/null
+++ b/.changeset/ai-sleepy-fox.md
@@ -0,0 +1,6 @@
+---
+"@module-federation/enhanced": patch
+---
+
+Use shareable runtime from federation global over custom global top levels
+```
diff --git a/.changeset/real-baboons-complain.md b/.changeset/real-baboons-complain.md
new file mode 100644
index 0000000000..7a0c6139d2
--- /dev/null
+++ b/.changeset/real-baboons-complain.md
@@ -0,0 +1,5 @@
+---
+'@module-federation/nextjs-mf': minor
+---
+
+support shareable runtime with experiments `use-host`. Defaults to `hoisted`
diff --git a/apps/3000-home/next-env.d.ts b/apps/3000-home/next-env.d.ts
index 4f11a03dc6..a4a7b3f5cf 100644
--- a/apps/3000-home/next-env.d.ts
+++ b/apps/3000-home/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/apps/3001-shop/next-env.d.ts b/apps/3001-shop/next-env.d.ts
index 4f11a03dc6..a4a7b3f5cf 100644
--- a/apps/3001-shop/next-env.d.ts
+++ b/apps/3001-shop/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/apps/3002-checkout/next-env.d.ts b/apps/3002-checkout/next-env.d.ts
index 4f11a03dc6..a4a7b3f5cf 100644
--- a/apps/3002-checkout/next-env.d.ts
+++ b/apps/3002-checkout/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts b/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts
index 644c68597b..7724410433 100644
--- a/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts
+++ b/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts
@@ -1,3 +1,7 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Zackary Jackson @ScriptedAlchemy
+*/
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import { moduleFederationPlugin } from '@module-federation/sdk';
import type {
diff --git a/packages/enhanced/src/lib/container/ContainerEntryDependency.ts b/packages/enhanced/src/lib/container/ContainerEntryDependency.ts
index 6552f89e5e..613018bea4 100644
--- a/packages/enhanced/src/lib/container/ContainerEntryDependency.ts
+++ b/packages/enhanced/src/lib/container/ContainerEntryDependency.ts
@@ -20,7 +20,6 @@ class ContainerEntryDependency extends Dependency {
public exposes: [string, ExposeOptions][];
public shareScope: string;
public injectRuntimeEntry: string;
- /** Additional experimental options for container plugin customization */
public experiments: containerPlugin.ContainerPluginOptions['experiments'];
public dataPrefetch: containerPlugin.ContainerPluginOptions['dataPrefetch'];
diff --git a/packages/enhanced/src/lib/container/ContainerPlugin.ts b/packages/enhanced/src/lib/container/ContainerPlugin.ts
index 0a5c533944..adba5dfe4f 100644
--- a/packages/enhanced/src/lib/container/ContainerPlugin.ts
+++ b/packages/enhanced/src/lib/container/ContainerPlugin.ts
@@ -226,7 +226,7 @@ class ContainerPlugin {
resolve(undefined);
},
);
- }).catch(callback);
+ }).catch((error) => callback(error));
await new Promise((resolve, reject) => {
compilation.addInclude(
@@ -253,7 +253,7 @@ class ContainerPlugin {
// we have to use finishMake in order to check the entries created and see if there are multiple runtime chunks
compiler.hooks.finishMake.tapAsync(
PLUGIN_NAME,
- (compilation: Compilation, callback) => {
+ async (compilation: Compilation, callback) => {
if (
compilation.compiler.parentCompilation &&
compilation.compiler.parentCompilation !== compilation
@@ -290,16 +290,46 @@ class ContainerPlugin {
dep.loc = { name };
- compilation.addInclude(
- compilation.options.context || '',
- dep,
- { name: undefined },
- (error: WebpackError | null | undefined) => {
- if (error) return callback(error);
- hooks.addContainerEntryModule.call(dep);
- callback();
- },
- );
+ await new Promise((resolve, reject) => {
+ compilation.addInclude(
+ compilation.options.context || '',
+ dep,
+ { name: undefined },
+ (error: WebpackError | null | undefined) => {
+ if (error) return reject(error);
+ hooks.addContainerEntryModule.call(dep);
+ resolve();
+ },
+ );
+ }).catch((error) => callback(error));
+
+ const addDependency = async (
+ dependency: FederationRuntimeDependency,
+ ) => {
+ await new Promise((resolve, reject) => {
+ compilation.addInclude(
+ compiler.context,
+ dependency,
+ { name: name, runtime: runtime },
+ (err, module) => {
+ if (err) return reject(err);
+ hooks.addFederationRuntimeModule.call(dependency);
+ resolve();
+ },
+ );
+ }).catch((error) => callback(error));
+ };
+
+ if (this._options?.experiments?.federationRuntime === 'use-host') {
+ const externalRuntimeDependency =
+ federationRuntimePluginInstance.getMinimalDependency(compiler);
+ await addDependency(externalRuntimeDependency);
+ } else {
+ const federationRuntimeDependency =
+ federationRuntimePluginInstance.getDependency(compiler);
+ await addDependency(federationRuntimeDependency);
+ }
+ callback();
},
);
@@ -316,6 +346,15 @@ class ContainerPlugin {
ContainerExposedDependency,
normalModuleFactory,
);
+
+ compilation.dependencyFactories.set(
+ FederationRuntimeDependency,
+ normalModuleFactory,
+ );
+ compilation.dependencyTemplates.set(
+ FederationRuntimeDependency,
+ new ModuleDependency.Template(),
+ );
},
);
diff --git a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts
index 92c07a95f3..03afb14993 100644
--- a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts
+++ b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts
@@ -25,7 +25,6 @@ export class HoistContainerReferences implements WebpackPluginInstance {
compiler.hooks.thisCompilation.tap(
PLUGIN_NAME,
(compilation: Compilation) => {
- const logger = compilation.getLogger(PLUGIN_NAME);
const hooks = FederationModulesPlugin.getCompilationHooks(compilation);
const containerEntryDependencies = new Set();
hooks.addContainerEntryModule.tap(
@@ -50,13 +49,7 @@ export class HoistContainerReferences implements WebpackPluginInstance {
},
(chunks: Iterable) => {
const runtimeChunks = this.getRuntimeChunks(compilation);
- this.hoistModulesInChunks(
- compilation,
- runtimeChunks,
- chunks,
- logger,
- containerEntryDependencies,
- );
+ this.hoistModulesInChunks(compilation, containerEntryDependencies);
},
);
},
@@ -66,14 +59,19 @@ export class HoistContainerReferences implements WebpackPluginInstance {
// Method to hoist modules in chunks
private hoistModulesInChunks(
compilation: Compilation,
- runtimeChunks: Set,
- chunks: Iterable,
- logger: ReturnType,
containerEntryDependencies: Set,
): 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
+
+ // First, handle the minimal check and remove included modules from the chunk
+ this.handleMinimalCheck(
+ compilation,
+ containerEntryDependencies,
+ chunkGraph,
+ moduleGraph,
+ );
+
+ // Now, perform the global hoist over all chunks
for (const dep of containerEntryDependencies) {
const containerEntryModule = moduleGraph.getModule(dep);
if (!containerEntryModule) continue;
@@ -81,12 +79,14 @@ export class HoistContainerReferences implements WebpackPluginInstance {
compilation,
containerEntryModule,
'initial',
+ true,
);
const allRemoteReferences = getAllReferencedModules(
compilation,
containerEntryModule,
'external',
+ true,
);
for (const remote of allRemoteReferences) {
@@ -122,6 +122,60 @@ export class HoistContainerReferences implements WebpackPluginInstance {
}
}
+ private handleMinimalCheck(
+ compilation: Compilation,
+ containerEntryDependencies: Set,
+ chunkGraph: Compilation['chunkGraph'],
+ moduleGraph: Compilation['moduleGraph'],
+ ): void {
+ let minimal;
+ for (const dep of containerEntryDependencies as Set) {
+ if (dep.minimal) {
+ minimal = moduleGraph.getModule(dep);
+ }
+ }
+ if (minimal) {
+ for (const dep of containerEntryDependencies as Set) {
+ if (dep.minimal) continue;
+ const containerEntryModule = moduleGraph.getModule(dep);
+ if (!containerEntryModule) continue;
+ const allReferencedModules = getAllReferencedModules(
+ compilation,
+ containerEntryModule,
+ 'initial',
+ );
+
+ const containerRuntimes =
+ chunkGraph.getModuleRuntimes(containerEntryModule);
+ const runtimes = new Set();
+
+ for (const runtimeSpec of containerRuntimes) {
+ 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;
+ // if there is no minimal chunk in the runtime module, skip it.
+ if (!chunkGraph.isModuleInChunk(minimal, runtimeChunk)) continue;
+
+ for (const module of allReferencedModules) {
+ if (chunkGraph.isModuleInChunk(module, runtimeChunk)) {
+ chunkGraph.disconnectChunkAndModule(runtimeChunk, module);
+ }
+ }
+ }
+ }
+ }
+ }
+
// Method to clean up chunks by disconnecting unused modules
private cleanUpChunks(compilation: Compilation, modules: Set): void {
const { chunkGraph } = compilation;
@@ -165,18 +219,20 @@ export function getAllReferencedModules(
compilation: Compilation,
module: Module,
type?: 'all' | 'initial' | 'external',
+ withInitialModule?: boolean,
): Set {
- const collectedModules = new Set([module]);
- const visitedModules = new WeakSet([module]);
+ const collectedModules = new Set(withInitialModule ? [module] : []);
+ const visitedModules = new WeakSet(withInitialModule ? [module] : []);
const stack = [module];
while (stack.length > 0) {
const currentModule = stack.pop();
if (!currentModule) continue;
- const mgm = compilation.moduleGraph._getModuleGraphModule(currentModule);
- if (!mgm?.outgoingConnections) continue;
- for (const connection of mgm.outgoingConnections) {
+ const outgoingConnections =
+ compilation.moduleGraph.getOutgoingConnections(currentModule);
+ if (!outgoingConnections) continue;
+ for (const connection of outgoingConnections) {
const connectedModule = connection.module;
// Skip if module has already been visited
diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
index 71cd86283f..36d2f7f7cc 100644
--- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
+++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
@@ -121,6 +121,7 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
) {
compiler.options.output.enabledLibraryTypes?.push(library.type);
}
+
compiler.hooks.afterPlugins.tap('ModuleFederationPlugin', () => {
if (useContainerPlugin) {
new ContainerPlugin({
diff --git a/packages/enhanced/src/lib/container/constant.ts b/packages/enhanced/src/lib/container/constant.ts
index 7994d851a5..7a49cf123c 100644
--- a/packages/enhanced/src/lib/container/constant.ts
+++ b/packages/enhanced/src/lib/container/constant.ts
@@ -1,3 +1,7 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Zackary Jackson @ScriptedAlchemy
+*/
import path from 'path';
import { TEMP_DIR as BasicTempDir } from '@module-federation/sdk';
diff --git a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts
index 79f333292c..c2e04610a7 100644
--- a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts
+++ b/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts
@@ -1,3 +1,8 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Zackary Jackson @ScriptedAlchemy
+*/
+
// This stores the previous child compilation based solution
// it is not currently used
diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts
index 34d00ef947..34b06126bb 100644
--- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts
+++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts
@@ -1,3 +1,7 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Zackary Jackson @ScriptedAlchemy
+*/
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import ContainerEntryDependency from '../ContainerEntryDependency';
@@ -12,42 +16,68 @@ class EmbedFederationRuntimeModule extends RuntimeModule {
private containerEntrySet: Set<
ContainerEntryDependency | FederationRuntimeDependency
>;
+
constructor(
containerEntrySet: Set<
ContainerEntryDependency | FederationRuntimeDependency
>,
) {
- super('embed federation', RuntimeModule.STAGE_ATTACH);
+ super('embed federation', RuntimeModule.STAGE_ATTACH - 1);
this.containerEntrySet = containerEntrySet;
}
+
override identifier() {
return 'webpack/runtime/embed/federation';
}
+
override generate(): string | null {
const { compilation, chunk, chunkGraph } = this;
if (!chunk || !chunkGraph || !compilation) {
return null;
}
+
let found;
- if (chunk.name) {
- for (const dep of this.containerEntrySet) {
- const mod = compilation.moduleGraph.getModule(dep);
- if (mod && compilation.chunkGraph.isModuleInChunk(mod, chunk)) {
+ let minimal;
+ for (const dep of this.containerEntrySet) {
+ const mod = compilation.moduleGraph.getModule(dep);
+ if (mod && compilation.chunkGraph.isModuleInChunk(mod, chunk)) {
+ //@ts-ignore
+ if (dep.minimal) {
+ minimal = mod as NormalModuleType;
+ } else {
found = mod as NormalModuleType;
- break;
}
}
}
- if (!found) {
+
+ if (!found && !minimal) {
return null;
}
- const initRuntimeModuleGetter = compilation.runtimeTemplate.moduleRaw({
- module: found,
- chunkGraph,
- request: found.request,
- weak: false,
- runtimeRequirements: new Set(),
- });
+
+ let initRuntimeModuleGetter = '';
+
+ if (found) {
+ initRuntimeModuleGetter = Template.asString([
+ compilation.runtimeTemplate.moduleRaw({
+ module: found,
+ chunkGraph,
+ request: found.request,
+ weak: false,
+ runtimeRequirements: new Set(),
+ }),
+ ]);
+ } else if (minimal) {
+ initRuntimeModuleGetter = Template.asString([
+ compilation.runtimeTemplate.moduleRaw({
+ module: minimal,
+ chunkGraph,
+ request: minimal.request,
+ weak: false,
+ runtimeRequirements: new Set(),
+ }),
+ ]);
+ }
+
return Template.asString([`${initRuntimeModuleGetter}`]);
}
}
diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts
index c5ce624e30..ec72bb565a 100644
--- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts
+++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts
@@ -1,7 +1,9 @@
+import type { Compiler, Compilation, Chunk } from 'webpack';
+import type { moduleFederationPlugin } from '@module-federation/sdk';
+
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import EmbedFederationRuntimeModule from './EmbedFederationRuntimeModule';
import FederationModulesPlugin from './FederationModulesPlugin';
-import type { Compiler, Compilation, Chunk } from 'webpack';
import { getFederationGlobalScope } from './utils';
import ContainerEntryDependency from '../ContainerEntryDependency';
import FederationRuntimeDependency from './FederationRuntimeDependency';
@@ -13,6 +15,14 @@ const { RuntimeGlobals } = require(
const federationGlobal = getFederationGlobalScope(RuntimeGlobals);
class EmbedFederationRuntimePlugin {
+ experiments: moduleFederationPlugin.ModuleFederationPluginOptions['experiments'];
+
+ constructor(
+ experiments: moduleFederationPlugin.ModuleFederationPluginOptions['experiments'],
+ ) {
+ this.experiments = experiments;
+ }
+
apply(compiler: Compiler): void {
compiler.hooks.thisCompilation.tap(
'EmbedFederationRuntimePlugin',
diff --git a/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts
index 9c79c0b7c8..eae38dce1e 100644
--- a/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts
+++ b/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts
@@ -9,10 +9,7 @@ import ContainerEntryDependency from '../ContainerEntryDependency';
import FederationRuntimeDependency from './FederationRuntimeDependency';
/** @type {WeakMap} */
-const compilationHooksMap = new WeakMap<
- import('webpack').Compilation,
- CompilationHooks
->();
+const compilationHooksMap = new WeakMap();
const PLUGIN_NAME = 'FederationModulesPlugin';
@@ -48,13 +45,14 @@ class FederationModulesPlugin {
}
constructor(options = {}) {
+ //@ts-ignore
this.options = options;
}
apply(compiler: Compiler) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
- (compilation: CompilationType, { normalModuleFactory }) => {
+ (compilation: CompilationType) => {
//@ts-ignore
const hooks = FederationModulesPlugin.getCompilationHooks(compilation);
},
diff --git a/packages/enhanced/src/lib/container/runtime/FederationRuntimeDependency.ts b/packages/enhanced/src/lib/container/runtime/FederationRuntimeDependency.ts
index 251f49622e..950edd35c7 100644
--- a/packages/enhanced/src/lib/container/runtime/FederationRuntimeDependency.ts
+++ b/packages/enhanced/src/lib/container/runtime/FederationRuntimeDependency.ts
@@ -5,11 +5,17 @@ const ModuleDependency = require(
) as typeof import('webpack/lib/dependencies/ModuleDependency');
class FederationRuntimeDependency extends ModuleDependency {
- constructor(request: string) {
+ minimal: boolean;
+
+ constructor(request: string, minimal = false) {
super(request);
+ this.minimal = minimal;
}
override get type() {
+ if (this.minimal) {
+ return 'minimal federation runtime dependency';
+ }
return 'federation runtime dependency';
}
}
diff --git a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts
index 37dc724f4d..6a0f1d502b 100644
--- a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts
+++ b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts
@@ -1,9 +1,13 @@
+import fs from 'fs';
+import path from 'path';
+import pBtoa from 'btoa';
import type {
Compiler,
WebpackPluginInstance,
Compilation,
Chunk,
} from 'webpack';
+import type { EntryDescription } from 'webpack/lib/Entrypoint';
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import { PrefetchPlugin } from '@module-federation/data-prefetch/cli';
import { moduleFederationPlugin } from '@module-federation/sdk';
@@ -15,13 +19,10 @@ import {
createHash,
normalizeToPosixPath,
} from './utils';
-import fs from 'fs';
-import path from 'path';
import { TEMP_DIR } from '../constant';
import EmbedFederationRuntimePlugin from './EmbedFederationRuntimePlugin';
import FederationModulesPlugin from './FederationModulesPlugin';
import HoistContainerReferences from '../HoistContainerReferencesPlugin';
-import pBtoa from 'btoa';
import FederationRuntimeDependency from './FederationRuntimeDependency';
const ModuleDependency = require(
@@ -43,6 +44,13 @@ const BundlerRuntimePath = require.resolve(
paths: [RuntimeToolsPath],
},
);
+
+const EmbeddedBundlerRuntimePath = require.resolve(
+ '@module-federation/webpack-bundler-runtime/embedded',
+ {
+ paths: [RuntimeToolsPath],
+ },
+);
const RuntimePath = require.resolve('@module-federation/runtime', {
paths: [RuntimeToolsPath],
});
@@ -55,31 +63,40 @@ const EmbeddedRuntimePath = require.resolve(
const federationGlobal = getFederationGlobalScope(RuntimeGlobals);
-const onceForCompler = new WeakSet();
+const onceForCompler = new WeakSet();
class FederationRuntimePlugin {
options?: moduleFederationPlugin.ModuleFederationPluginOptions;
entryFilePath: string;
bundlerRuntimePath: string;
- federationRuntimeDependency?: FederationRuntimeDependency; // Add this line
+ embeddedBundlerRuntimePath: string;
+ embeddedEntryFilePath: string;
+ federationRuntimeDependency?: FederationRuntimeDependency;
+ minimalFederationRuntimeDependency?: FederationRuntimeDependency;
constructor(options?: moduleFederationPlugin.ModuleFederationPluginOptions) {
this.options = options ? { ...options } : undefined;
this.entryFilePath = '';
this.bundlerRuntimePath = BundlerRuntimePath;
- this.federationRuntimeDependency = undefined; // Initialize as undefined
+ this.federationRuntimeDependency = undefined;
+ this.minimalFederationRuntimeDependency = undefined;
+ this.embeddedBundlerRuntimePath = EmbeddedBundlerRuntimePath;
+ this.embeddedEntryFilePath = '';
}
static getTemplate(
compiler: Compiler,
options: moduleFederationPlugin.ModuleFederationPluginOptions,
bundlerRuntimePath?: string,
- experiments?: moduleFederationPlugin.ModuleFederationPluginOptions['experiments'],
+ embeddedBundlerRuntimePath?: string,
+ useMinimalRuntime = false,
) {
// internal runtime plugin
const runtimePlugins = options.runtimePlugins;
const normalizedBundlerRuntimePath = normalizeToPosixPath(
- bundlerRuntimePath || BundlerRuntimePath,
+ useMinimalRuntime
+ ? embeddedBundlerRuntimePath || EmbeddedBundlerRuntimePath
+ : bundlerRuntimePath || BundlerRuntimePath,
);
let runtimePluginTemplates = '';
@@ -99,7 +116,6 @@ class FederationRuntimePlugin {
});
}
const embedRuntimeLines = Template.asString([
- `if(!${federationGlobal}.runtime){`,
Template.indent([
`var prevFederation = ${federationGlobal};`,
`${federationGlobal} = {}`,
@@ -110,9 +126,43 @@ class FederationRuntimePlugin {
Template.indent([`${federationGlobal}[key] = prevFederation[key];`]),
'}',
]),
- '}',
]);
+ if (useMinimalRuntime) {
+ return Template.asString([
+ `import federation from '${normalizedBundlerRuntimePath}';`,
+ runtimePluginTemplates,
+ embedRuntimeLines,
+ `if(!${federationGlobal}.instance){`,
+ Template.indent([
+ runtimePluginNames.length
+ ? Template.asString([
+ `const pluginsToAdd = [`,
+ Template.indent(
+ runtimePluginNames.map(
+ (item) =>
+ `${item} ? (${item}.default || ${item})() : false,`,
+ ),
+ ),
+ `].filter(Boolean);`,
+ `${federationGlobal}.initOptions.plugins = ${federationGlobal}.initOptions.plugins ? `,
+ `${federationGlobal}.initOptions.plugins.concat(pluginsToAdd) : pluginsToAdd;`,
+ ])
+ : '',
+ ]),
+ `${federationGlobal}.instance = federation.runtime.init(${federationGlobal}.initOptions);`,
+ `if(${federationGlobal}.attachShareScopeMap){`,
+ Template.indent([
+ `${federationGlobal}.attachShareScopeMap(${RuntimeGlobals.require})`,
+ ]),
+ '}',
+ `if(${federationGlobal}.installInitialConsumes){`,
+ Template.indent([`${federationGlobal}.installInitialConsumes()`]),
+ '}',
+ `}`,
+ ]);
+ }
+
return Template.asString([
`import federation from '${normalizedBundlerRuntimePath}';`,
runtimePluginTemplates,
@@ -153,7 +203,8 @@ class FederationRuntimePlugin {
compiler: Compiler,
options: moduleFederationPlugin.ModuleFederationPluginOptions,
bundlerRuntimePath?: string,
- experiments?: moduleFederationPlugin.ModuleFederationPluginOptions['experiments'],
+ embeddedBundlerRuntimePath?: string,
+ useMinimalRuntime = false,
) {
const containerName = options.name;
const hash = createHash(
@@ -161,40 +212,51 @@ class FederationRuntimePlugin {
compiler,
options,
bundlerRuntimePath,
- experiments,
+ embeddedBundlerRuntimePath,
+ useMinimalRuntime,
)}`,
);
return path.join(TEMP_DIR, `entry.${hash}.js`);
}
- getFilePath(compiler: Compiler) {
- if (this.entryFilePath) {
- return this.entryFilePath;
- }
-
+ getFilePath(compiler: Compiler, useMinimalRuntime = false) {
if (!this.options) {
return '';
}
- if (!this.options?.virtualRuntimeEntry) {
- this.entryFilePath = FederationRuntimePlugin.getFilePath(
- compiler,
- this.options,
- this.bundlerRuntimePath,
- this.options.experiments,
- );
- } else {
- this.entryFilePath = `data:text/javascript;charset=utf-8;base64,${pBtoa(
- FederationRuntimePlugin.getTemplate(
+ const cachedFilePath = useMinimalRuntime
+ ? this.embeddedEntryFilePath
+ : this.entryFilePath;
+ if (cachedFilePath) {
+ return cachedFilePath;
+ }
+
+ const filePath = this.options.virtualRuntimeEntry
+ ? `data:text/javascript;charset=utf-8;base64,${pBtoa(
+ FederationRuntimePlugin.getTemplate(
+ compiler,
+ this.options,
+ this.bundlerRuntimePath,
+ this.embeddedBundlerRuntimePath,
+ useMinimalRuntime,
+ ),
+ )}`
+ : FederationRuntimePlugin.getFilePath(
compiler,
this.options,
this.bundlerRuntimePath,
- this.options.experiments,
- ),
- )}`;
+ this.embeddedBundlerRuntimePath,
+ useMinimalRuntime,
+ );
+
+ if (useMinimalRuntime) {
+ this.embeddedEntryFilePath = filePath;
+ } else {
+ this.entryFilePath = filePath;
}
- return this.entryFilePath;
+
+ return filePath;
}
- ensureFile(compiler: Compiler) {
+ ensureFile(compiler: Compiler, useMinimalRuntime = false) {
if (!this.options) {
return;
}
@@ -202,7 +264,7 @@ class FederationRuntimePlugin {
if (this.options?.virtualRuntimeEntry) {
return;
}
- const filePath = this.getFilePath(compiler);
+ const filePath = this.getFilePath(compiler, useMinimalRuntime);
try {
fs.readFileSync(filePath);
} catch (err) {
@@ -213,7 +275,8 @@ class FederationRuntimePlugin {
compiler,
this.options,
this.bundlerRuntimePath,
- this.options.experiments,
+ this.embeddedBundlerRuntimePath,
+ useMinimalRuntime,
),
);
}
@@ -231,10 +294,25 @@ class FederationRuntimePlugin {
return this.federationRuntimeDependency;
}
+ getMinimalDependency(compiler: Compiler) {
+ if (this.minimalFederationRuntimeDependency)
+ return this.minimalFederationRuntimeDependency;
+ this.minimalFederationRuntimeDependency = new FederationRuntimeDependency(
+ this.getFilePath(compiler, true),
+ true,
+ );
+ return this.minimalFederationRuntimeDependency;
+ }
+
prependEntry(compiler: Compiler) {
if (!this.options?.virtualRuntimeEntry) {
this.ensureFile(compiler);
}
+ const useHost = this.options?.experiments?.federationRuntime === 'use-host';
+
+ if (useHost) {
+ this.ensureFile(compiler, true);
+ }
//if using runtime experiment, use the new include method else patch entry
if (this.options?.experiments?.federationRuntime) {
@@ -277,7 +355,7 @@ class FederationRuntimePlugin {
const entryFilePath = this.getFilePath(compiler);
modifyEntry({
compiler,
- prependEntry: (entry) => {
+ prependEntry: (entry: Record) => {
Object.keys(entry).forEach((entryName) => {
const entryItem = entry[entryName];
if (!entryItem.import) {
@@ -359,17 +437,19 @@ class FederationRuntimePlugin {
setRuntimeAlias(compiler: Compiler) {
const { experiments, implementation } = this.options || {};
- const isHoisted = experiments?.federationRuntime === 'hoisted';
- let runtimePath = isHoisted ? EmbeddedRuntimePath : RuntimePath;
+ const useExperimentalRuntime = experiments?.federationRuntime;
+ let runtimePath = useExperimentalRuntime
+ ? EmbeddedRuntimePath
+ : RuntimePath;
if (implementation) {
runtimePath = require.resolve(
- `@module-federation/runtime${isHoisted ? '/embedded' : ''}`,
+ `@module-federation/runtime${useExperimentalRuntime ? '/embedded' : ''}`,
{ paths: [implementation] },
);
}
- if (isHoisted) {
+ if (useExperimentalRuntime) {
runtimePath = runtimePath.replace('.cjs', '.esm');
}
@@ -397,7 +477,6 @@ class FederationRuntimePlugin {
);
if (useModuleFederationPlugin && !this.options) {
- // @ts-ignore
this.options = useModuleFederationPlugin._options;
}
@@ -434,20 +513,34 @@ class FederationRuntimePlugin {
paths: [this.options.implementation],
},
);
+
+ this.embeddedBundlerRuntimePath = require.resolve(
+ '@module-federation/webpack-bundler-runtime/embedded',
+ {
+ paths: [this.options.implementation],
+ },
+ );
}
- if (this.options?.experiments?.federationRuntime === 'hoisted') {
+ if (this.options?.experiments?.federationRuntime) {
this.bundlerRuntimePath = this.bundlerRuntimePath.replace(
'.cjs.js',
'.esm.js',
);
- new EmbedFederationRuntimePlugin().apply(compiler);
+ this.embeddedBundlerRuntimePath = this.embeddedBundlerRuntimePath.replace(
+ '.cjs.js',
+ '.esm.js',
+ );
+
+ new EmbedFederationRuntimePlugin(this.options.experiments).apply(
+ compiler,
+ );
new HoistContainerReferences().apply(compiler);
new compiler.webpack.NormalModuleReplacementPlugin(
- /@module-federation\/runtime/,
+ /@module-federation\/runtime(?!\/embedded)/,
(resolveData) => {
if (/webpack-bundler-runtime/.test(resolveData.contextInfo.issuer)) {
resolveData.request = RuntimePath.replace('cjs', 'esm');
diff --git a/packages/enhanced/src/lib/container/runtime/utils.ts b/packages/enhanced/src/lib/container/runtime/utils.ts
index b8f1f2ea3e..18886f0ff0 100644
--- a/packages/enhanced/src/lib/container/runtime/utils.ts
+++ b/packages/enhanced/src/lib/container/runtime/utils.ts
@@ -1,3 +1,7 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Zackary Jackson @ScriptedAlchemy
+*/
import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path';
import upath from 'upath';
import path from 'path';
diff --git a/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts b/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts
index d7ccb04b3b..443758569d 100644
--- a/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts
+++ b/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts
@@ -297,7 +297,7 @@ const schema21 = {
type: 'object',
properties: {
federationRuntime: {
- anyOf: [{ type: 'boolean' }, { enum: ['hoisted'] }],
+ anyOf: [{ type: 'boolean' }, { enum: ['hoisted', 'use-host'] }],
},
},
additionalProperties: false,
diff --git a/packages/enhanced/src/schemas/container/ContainerPlugin.ts b/packages/enhanced/src/schemas/container/ContainerPlugin.ts
index bd4fc7b791..2b25b677b5 100644
--- a/packages/enhanced/src/schemas/container/ContainerPlugin.ts
+++ b/packages/enhanced/src/schemas/container/ContainerPlugin.ts
@@ -341,7 +341,7 @@ export default {
type: 'object',
properties: {
federationRuntime: {
- anyOf: [{ type: 'boolean' }, { enum: ['hoisted'] }],
+ anyOf: [{ type: 'boolean' }, { enum: ['hoisted', 'use-host'] }],
},
},
additionalProperties: false,
diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts
index 8aaa2e66f3..a80aa37672 100644
--- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts
+++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/index.ts
@@ -213,7 +213,8 @@ export class NextFederationPlugin {
dts: this._options.dts ?? false,
shareStrategy: this._options.shareStrategy ?? 'loaded-first',
experiments: {
- federationRuntime: 'hoisted',
+ federationRuntime:
+ this._options.experiments?.federationRuntime || 'hoisted',
},
};
}
diff --git a/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts b/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts
index 9300a5bd6a..5535c07ea0 100644
--- a/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts
+++ b/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts
@@ -15,18 +15,10 @@ class InvertedContainerRuntimeModule extends RuntimeModule {
private options: InvertedContainerRuntimeModuleOptions;
constructor(options: InvertedContainerRuntimeModuleOptions) {
- super('inverted container startup', RuntimeModule.STAGE_TRIGGER);
+ super('inverted container startup', RuntimeModule.STAGE_ATTACH);
this.options = options;
}
- private findEntryModuleOfContainer(): Module | undefined {
- if (!this.chunk || !this.chunkGraph) return undefined;
- const modules = this.chunkGraph.getChunkModules(this.chunk);
- return Array.from(modules).find(
- (module) => module instanceof container.ContainerEntryModule,
- );
- }
-
override generate(): string {
const { compilation, chunk, chunkGraph } = this;
if (!compilation || !chunk || !chunkGraph) {
@@ -46,6 +38,12 @@ class InvertedContainerRuntimeModule extends RuntimeModule {
if (!containerEntryModule) return '';
+ if (
+ compilation.chunkGraph.isEntryModuleInChunk(containerEntryModule, chunk)
+ ) {
+ // dont apply to remote entry itself
+ return '';
+ }
const initRuntimeModuleGetter = compilation.runtimeTemplate.moduleRaw({
module: containerEntryModule,
chunkGraph,
diff --git a/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap b/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
index 2d845938dc..f9a9578ed2 100644
--- a/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
+++ b/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
@@ -32,7 +32,7 @@ exports[`preload-remote inBrowser > 1 preload with default config 1`] = `
}
`;
-exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
+exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
{
"links": [
{
@@ -89,7 +89,7 @@ exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
}
`;
-exports[`preload-remote inBrowser > 3 preload with expose config 1`] = `
+exports[`preload-remote inBrowser > 3 preload with expose config 1`] = `
{
"links": [
{
@@ -107,7 +107,7 @@ exports[`preload-remote inBrowser > 3 preload with expose config 1`] = `
}
`;
-exports[`preload-remote inBrowser > 3 preload with expose config 2`] = `
+exports[`preload-remote inBrowser > 3 preload with expose config 2`] = `
{
"links": [
{
diff --git a/packages/runtime/__tests__/globa.spec.ts b/packages/runtime/__tests__/globa.spec.ts
index e82036cae5..233d030f44 100644
--- a/packages/runtime/__tests__/globa.spec.ts
+++ b/packages/runtime/__tests__/globa.spec.ts
@@ -14,6 +14,7 @@ describe('global', () => {
);
expect(globalThis.__FEDERATION__.__DEBUG_CONSTRUCTOR__).toBeCalledWith(
injectArgs,
+ '',
);
});
});
diff --git a/packages/runtime/__tests__/global.spec.ts b/packages/runtime/__tests__/global.spec.ts
index 7b9a07203d..9c0332c7fe 100644
--- a/packages/runtime/__tests__/global.spec.ts
+++ b/packages/runtime/__tests__/global.spec.ts
@@ -13,9 +13,9 @@ describe('global', () => {
expect(GM.constructor).toBe(
globalThis.__FEDERATION__.__DEBUG_CONSTRUCTOR__,
);
- expect(globalThis.__FEDERATION__.__DEBUG_CONSTRUCTOR__).toBeCalledWith(
- injectArgs,
- );
+ expect(
+ globalThis.__FEDERATION__.__DEBUG_CONSTRUCTOR__,
+ ).toHaveBeenCalledWith(injectArgs, '');
});
it('getInfoWithoutType', () => {
diff --git a/packages/runtime/__tests__/sync.spec.ts b/packages/runtime/__tests__/sync.spec.ts
index 929c434350..231e5325e3 100644
--- a/packages/runtime/__tests__/sync.spec.ts
+++ b/packages/runtime/__tests__/sync.spec.ts
@@ -39,7 +39,9 @@ describe('Embed Module Proxy', async () => {
it('should have the same exports in embedded.ts and index.ts', () => {
// Compare the exports of embedded.ts and index.ts
const embeddedExports = Object.keys(Embedded).sort();
- const indexExports = Object.keys(Index).sort();
+ const indexExports = Object.keys(Index)
+ .sort()
+ .filter((n) => n !== 'FederationManager');
expect(embeddedExports).toEqual(indexExports);
});
diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts
index c129dc62df..6e53466213 100644
--- a/packages/runtime/src/core.ts
+++ b/packages/runtime/src/core.ts
@@ -117,11 +117,11 @@ export class FederationHost {
>(),
});
- constructor(userOptions: UserOptions) {
+ constructor(userOptions: UserOptions, bundlerId?: string) {
// TODO: Validate the details of the options
// Initialize options with default values
const defaultOptions: Options = {
- id: getBuilderId(),
+ id: bundlerId || getBuilderId(),
name: userOptions.name,
plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()],
remotes: [],
diff --git a/packages/runtime/src/global.ts b/packages/runtime/src/global.ts
index 27c6b28238..f47188b469 100644
--- a/packages/runtime/src/global.ts
+++ b/packages/runtime/src/global.ts
@@ -15,6 +15,18 @@ import { getBuilderId } from './utils/env';
import { warn } from './utils/logger';
import { FederationRuntimePlugin } from './type/plugin';
+// Define a type for the shareable runtime
+type ShareableRuntime = {
+ FederationManager: typeof import('./index').FederationManager;
+ FederationHost: typeof import('./index').FederationHost;
+ loadScript: typeof import('./index').loadScript;
+ loadScriptNode: typeof import('./index').loadScriptNode;
+ registerGlobalPlugins: typeof import('./index').registerGlobalPlugins;
+ getRemoteInfo: typeof import('./index').getRemoteInfo;
+ getRemoteEntry: typeof import('./index').getRemoteEntry;
+ Module: typeof import('./index').Module;
+};
+
export interface Federation {
__GLOBAL_PLUGIN__: Array;
__DEBUG_CONSTRUCTOR_VERSION__?: string;
@@ -24,6 +36,7 @@ export interface Federation {
__SHARE__: GlobalShareScopeMap;
__MANIFEST_LOADING__: Record>;
__PRELOADED_MAP__: Map;
+ __SHAREABLE_RUNTIME__: ShareableRuntime | undefined;
}
export const nativeGlobal: typeof global = (() => {
@@ -88,6 +101,7 @@ function setGlobalDefaultVal(target: typeof globalThis) {
__SHARE__: {},
__MANIFEST_LOADING__: {},
__PRELOADED_MAP__: new Map(),
+ __SHAREABLE_RUNTIME__: undefined,
});
definePropertyGlobalVal(target, '__VMOK__', target.__FEDERATION__);
@@ -99,6 +113,7 @@ function setGlobalDefaultVal(target: typeof globalThis) {
target.__FEDERATION__.__SHARE__ ??= {};
target.__FEDERATION__.__MANIFEST_LOADING__ ??= {};
target.__FEDERATION__.__PRELOADED_MAP__ ??= new Map();
+ target.__FEDERATION__.__SHAREABLE_RUNTIME__ ??= undefined;
}
setGlobalDefaultVal(globalThis);
@@ -115,10 +130,11 @@ export function resetFederationGlobalInfo(): void {
export function getGlobalFederationInstance(
name: string,
version: string | undefined,
+ builderId?: string | undefined,
): FederationHost | undefined {
- const buildId = getBuilderId();
+ const buildId = builderId || getBuilderId();
return globalThis.__FEDERATION__.__INSTANCES__.find((GMInstance) => {
- if (buildId && GMInstance.options.id === getBuilderId()) {
+ if (buildId && GMInstance.options.id === (builderId || getBuilderId())) {
return true;
}
@@ -309,7 +325,16 @@ export const getGlobalHostPlugins = (): Array =>
nativeGlobal.__FEDERATION__.__GLOBAL_PLUGIN__;
export const getPreloaded = (id: string) =>
- globalThis.__FEDERATION__.__PRELOADED_MAP__.get(id);
+ nativeGlobal.__FEDERATION__.__PRELOADED_MAP__.get(id);
export const setPreloaded = (id: string) =>
- globalThis.__FEDERATION__.__PRELOADED_MAP__.set(id, true);
+ nativeGlobal.__FEDERATION__.__PRELOADED_MAP__.set(id, true);
+
+export function setGlobalShareableRuntime(
+ runtimeExports: ShareableRuntime,
+): void {
+ if (nativeGlobal.__FEDERATION__.__SHAREABLE_RUNTIME__) {
+ return;
+ }
+ nativeGlobal.__FEDERATION__.__SHAREABLE_RUNTIME__ = runtimeExports;
+}
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 2d0eec68af..64438ef74f 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -6,95 +6,163 @@ import {
setGlobalFederationConstructor,
} from './global';
import { UserOptions, FederationRuntimePlugin } from './type';
+import { getBuilderId, getRemoteEntry, getRemoteInfo } from './utils';
import { assert } from './utils/logger';
export { FederationHost } from './core';
export { registerGlobalPlugins } from './global';
+import { registerGlobalPlugins, setGlobalShareableRuntime } from './global';
export { getRemoteEntry, getRemoteInfo } from './utils';
export { loadScript, loadScriptNode } from '@module-federation/sdk';
+import { loadScript, loadScriptNode } from '@module-federation/sdk';
export { Module } from './module';
-
+import { Module } from './module';
export type { Federation } from './global';
export type { FederationRuntimePlugin };
-let FederationInstance: FederationHost | null = null;
-export function init(options: UserOptions): FederationHost {
- // Retrieve the same instance with the same name
- const instance = getGlobalFederationInstance(options.name, options.version);
- if (!instance) {
- // Retrieve debug constructor
- const FederationConstructor =
- getGlobalFederationConstructor() || FederationHost;
- FederationInstance = new FederationConstructor(options);
- setGlobalFederationInstance(FederationInstance);
- return FederationInstance;
- } else {
- // Merge options
- instance.initOptions(options);
- if (!FederationInstance) {
- FederationInstance = instance;
+export class FederationManager {
+ private federationInstance: FederationHost | null = null;
+ private _bundlerId: string; // Add this line to declare the property
+
+ constructor(bundlerId?: string) {
+ this._bundlerId = bundlerId || getBuilderId();
+ setGlobalFederationConstructor(FederationHost);
+ }
+ init(options: UserOptions): FederationHost {
+ // Retrieve the same instance with the same name
+ const instance = getGlobalFederationInstance(
+ options.name,
+ options.version,
+ this._bundlerId,
+ );
+ if (!instance) {
+ // Retrieve debug constructor
+ const FederationConstructor =
+ getGlobalFederationConstructor() || FederationHost;
+ this.federationInstance = new FederationConstructor(
+ options,
+ this._bundlerId,
+ );
+ setGlobalFederationInstance(this.federationInstance);
+ return this.federationInstance;
+ } else {
+ // Merge options
+ instance.initOptions(options);
+ if (!this.federationInstance) {
+ this.federationInstance = instance;
+ }
+ return instance;
}
- return instance;
+ }
+
+ loadRemote(
+ ...args: Parameters
+ ): Promise {
+ assert(this.federationInstance, 'Please call init first');
+ const loadRemote: typeof this.federationInstance.loadRemote =
+ this.federationInstance.loadRemote;
+ return loadRemote.apply(this.federationInstance, args);
+ }
+
+ loadShare(
+ ...args: Parameters
+ ): Promise T | undefined)> {
+ assert(this.federationInstance, 'Please call init first');
+ const loadShare: typeof this.federationInstance.loadShare =
+ this.federationInstance.loadShare;
+ return loadShare.apply(this.federationInstance, args);
+ }
+
+ loadShareSync(
+ ...args: Parameters
+ ): () => T | never {
+ assert(this.federationInstance, 'Please call init first');
+ const loadShareSync: typeof this.federationInstance.loadShareSync =
+ this.federationInstance.loadShareSync;
+ return loadShareSync.apply(this.federationInstance, args);
+ }
+
+ preloadRemote(
+ ...args: Parameters
+ ): ReturnType {
+ assert(this.federationInstance, 'Please call init first');
+ return this.federationInstance.preloadRemote(...args); // Use spread operator
+ }
+
+ registerRemotes(
+ ...args: Parameters
+ ): ReturnType {
+ assert(this.federationInstance, 'Please call init first');
+ return this.federationInstance.registerRemotes(...args); // Use spread operator
+ }
+
+ registerPlugins(
+ ...args: Parameters
+ ): ReturnType {
+ assert(this.federationInstance, 'Please call init first');
+ return this.federationInstance.registerPlugins(...args); // Use spread operator
+ }
+
+ getInstance() {
+ return this.federationInstance;
}
}
+// Create a singleton instance of the Federation class
+const federation = new FederationManager();
+
+// Re-export the functions with the same names
+export function init(options: UserOptions): FederationHost {
+ return federation.init(options);
+}
+
export function loadRemote(
...args: Parameters
): Promise {
- assert(FederationInstance, 'Please call init first');
- const loadRemote: typeof FederationInstance.loadRemote =
- FederationInstance.loadRemote;
- // eslint-disable-next-line prefer-spread
- return loadRemote.apply(FederationInstance, args);
+ return federation.loadRemote(...args);
}
export function loadShare(
...args: Parameters
): Promise T | undefined)> {
- assert(FederationInstance, 'Please call init first');
- // eslint-disable-next-line prefer-spread
- const loadShare: typeof FederationInstance.loadShare =
- FederationInstance.loadShare;
- return loadShare.apply(FederationInstance, args);
+ return federation.loadShare(...args);
}
export function loadShareSync(
...args: Parameters
): () => T | never {
- assert(FederationInstance, 'Please call init first');
- const loadShareSync: typeof FederationInstance.loadShareSync =
- FederationInstance.loadShareSync;
- // eslint-disable-next-line prefer-spread
- return loadShareSync.apply(FederationInstance, args);
+ return federation.loadShareSync(...args);
}
export function preloadRemote(
...args: Parameters
): ReturnType {
- assert(FederationInstance, 'Please call init first');
- // eslint-disable-next-line prefer-spread
- return FederationInstance.preloadRemote.apply(FederationInstance, args);
+ return federation.preloadRemote(...args);
}
export function registerRemotes(
...args: Parameters
): ReturnType {
- assert(FederationInstance, 'Please call init first');
- // eslint-disable-next-line prefer-spread
- return FederationInstance.registerRemotes.apply(FederationInstance, args);
+ return federation.registerRemotes(...args);
}
export function registerPlugins(
...args: Parameters
-): ReturnType {
- assert(FederationInstance, 'Please call init first');
- // eslint-disable-next-line prefer-spread
- return FederationInstance.registerPlugins.apply(FederationInstance, args);
+): ReturnType {
+ return federation.registerPlugins(...args);
}
export function getInstance() {
- return FederationInstance;
+ return federation.getInstance();
}
-// Inject for debug
-setGlobalFederationConstructor(FederationHost);
+setGlobalShareableRuntime({
+ FederationManager,
+ FederationHost,
+ loadScript,
+ loadScriptNode,
+ registerGlobalPlugins,
+ getRemoteInfo,
+ getRemoteEntry,
+ Module,
+});
diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts
index 0b0bf556e2..de84d31e19 100644
--- a/packages/runtime/src/remote/index.ts
+++ b/packages/runtime/src/remote/index.ts
@@ -139,7 +139,7 @@ export class RemoteHandler {
loadEntry: new AsyncHook<
[
{
- createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript'];
+ loaderHook: FederationHost['loaderHook'];
remoteInfo: RemoteInfo;
remoteEntryExports?: RemoteEntryExports;
},
diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts
index 2fac3c3632..418e49ec0e 100644
--- a/packages/runtime/src/utils/load.ts
+++ b/packages/runtime/src/utils/load.ts
@@ -63,12 +63,12 @@ async function loadEntryScript({
name,
globalName,
entry,
- createScriptHook,
+ loaderHook,
}: {
name: string;
globalName: string;
entry: string;
- createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript'];
+ loaderHook: FederationHost['loaderHook'];
}): Promise {
const { entryExports: remoteEntryExports } = getRemoteEntryExports(
name,
@@ -82,7 +82,7 @@ async function loadEntryScript({
return loadScript(entry, {
attrs: {},
createScriptHook: (url, attrs) => {
- const res = createScriptHook.emit({ url, attrs });
+ const res = loaderHook.lifecycle.createScript.emit({ url, attrs });
if (!res) return;
@@ -123,11 +123,11 @@ async function loadEntryScript({
async function loadEntryDom({
remoteInfo,
remoteEntryExports,
- createScriptHook,
+ loaderHook,
}: {
remoteInfo: RemoteInfo;
remoteEntryExports?: RemoteEntryExports;
- createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript'];
+ loaderHook: FederationHost['loaderHook'];
}) {
const { entry, entryGlobalName: globalName, name, type } = remoteInfo;
switch (type) {
@@ -137,16 +137,16 @@ async function loadEntryDom({
case 'system':
return loadSystemJsEntry({ entry, remoteEntryExports });
default:
- return loadEntryScript({ entry, globalName, name, createScriptHook });
+ return loadEntryScript({ entry, globalName, name, loaderHook });
}
}
async function loadEntryNode({
remoteInfo,
- createScriptHook,
+ loaderHook,
}: {
remoteInfo: RemoteInfo;
- createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript'];
+ loaderHook: FederationHost['loaderHook'];
}) {
const { entry, entryGlobalName: globalName, name, type } = remoteInfo;
const { entryExports: remoteEntryExports } = getRemoteEntryExports(
@@ -160,16 +160,18 @@ async function loadEntryNode({
return loadScriptNode(entry, {
attrs: { name, globalName, type },
- createScriptHook: (url, attrs) => {
- const res = createScriptHook.emit({ url, attrs });
+ loaderHook: {
+ createScriptHook: (url, attrs) => {
+ const res = loaderHook.lifecycle.createScript.emit({ url, attrs });
- if (!res) return;
+ if (!res) return;
- if ('url' in res) {
- return res;
- }
+ if ('url' in res) {
+ return res;
+ }
- return;
+ return;
+ },
},
})
.then(() => {
@@ -217,9 +219,11 @@ export async function getRemoteEntry({
if (!globalLoading[uniqueKey]) {
const loadEntryHook = origin.remoteHandler.hooks.lifecycle.loadEntry;
const createScriptHook = origin.loaderHook.lifecycle.createScript;
+ const loaderHook = origin.loaderHook;
+
globalLoading[uniqueKey] = loadEntryHook
.emit({
- createScriptHook,
+ loaderHook,
remoteInfo,
remoteEntryExports,
})
@@ -228,8 +232,8 @@ export async function getRemoteEntry({
return res;
}
return isBrowserEnv()
- ? loadEntryDom({ remoteInfo, remoteEntryExports, createScriptHook })
- : loadEntryNode({ remoteInfo, createScriptHook });
+ ? loadEntryDom({ remoteInfo, remoteEntryExports, loaderHook })
+ : loadEntryNode({ remoteInfo, loaderHook });
});
}
diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts
index 0a4ae1433c..c774a6d257 100644
--- a/packages/sdk/src/node.ts
+++ b/packages/sdk/src/node.ts
@@ -1,4 +1,4 @@
-import { CreateScriptHookNode } from './types';
+import { CreateScriptHookNode, FetchHook } from './types';
function importNodeModule(name: string): Promise {
if (!name) {
@@ -22,12 +22,10 @@ const loadNodeFetch = async (): Promise => {
const lazyLoaderHookFetch = async (
input: RequestInfo | URL,
init?: RequestInit,
+ loaderHook?: any,
): Promise => {
- // @ts-ignore
- const loaderHooks = __webpack_require__.federation.instance.loaderHook;
-
const hook = (url: RequestInfo | URL, init: RequestInit) => {
- return loaderHooks.lifecycle.fetch.emit(url, init);
+ return loaderHook.lifecycle.fetch.emit(url, init);
};
const res = await hook(input, init || {});
@@ -44,10 +42,13 @@ export function createScriptNode(
url: string,
cb: (error?: Error, scriptContext?: any) => void,
attrs?: Record,
- createScriptHook?: CreateScriptHookNode,
+ loaderHook?: {
+ createScriptHook?: CreateScriptHookNode;
+ fetch?: FetchHook;
+ },
) {
- if (createScriptHook) {
- const hookResult = createScriptHook(url);
+ if (loaderHook?.createScriptHook) {
+ const hookResult = loaderHook.createScriptHook(url);
if (hookResult && typeof hookResult === 'object' && 'url' in hookResult) {
url = hookResult.url;
}
@@ -63,20 +64,9 @@ export function createScriptNode(
}
const getFetch = async (): Promise => {
- //@ts-ignore
- if (typeof __webpack_require__ !== 'undefined') {
- try {
- //@ts-ignore
- const loaderHooks = __webpack_require__.federation.instance.loaderHook;
- if (loaderHooks.lifecycle.fetch) {
- return lazyLoaderHookFetch;
- }
- } catch (e) {
- console.warn(
- 'federation.instance.loaderHook.lifecycle.fetch failed:',
- e,
- );
- }
+ if (loaderHook?.fetch) {
+ return (input: RequestInfo | URL, init?: RequestInit) =>
+ lazyLoaderHookFetch(input, init, loaderHook);
}
return typeof fetch === 'undefined' ? loadNodeFetch() : fetch;
@@ -162,7 +152,9 @@ export function loadScriptNode(
url: string,
info: {
attrs?: Record;
- createScriptHook?: CreateScriptHookNode;
+ loaderHook?: {
+ createScriptHook?: CreateScriptHookNode;
+ };
},
) {
return new Promise((resolve, reject) => {
@@ -181,7 +173,7 @@ export function loadScriptNode(
}
},
info.attrs,
- info.createScriptHook,
+ info.loaderHook,
);
});
}
diff --git a/packages/sdk/src/types/hooks.ts b/packages/sdk/src/types/hooks.ts
index 1284d890e0..75d4326b82 100644
--- a/packages/sdk/src/types/hooks.ts
+++ b/packages/sdk/src/types/hooks.ts
@@ -23,3 +23,7 @@ export type CreateScriptHook = (
url: string,
attrs?: Record | undefined,
) => CreateScriptHookReturn;
+
+export type FetchHook = (
+ args: [string, RequestInit],
+) => Promise | void | false;
diff --git a/packages/sdk/src/types/plugins/ContainerPlugin.ts b/packages/sdk/src/types/plugins/ContainerPlugin.ts
index da350d5170..a72121660b 100644
--- a/packages/sdk/src/types/plugins/ContainerPlugin.ts
+++ b/packages/sdk/src/types/plugins/ContainerPlugin.ts
@@ -100,7 +100,7 @@ export interface ContainerPluginOptions {
runtimePlugins?: string[];
experiments?: {
- federationRuntime?: false | 'hoisted';
+ federationRuntime?: false | 'hoisted' | 'use-host';
};
dataPrefetch?: DataPrefetch;
}
diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
index e31c6169ec..37117e495e 100644
--- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
+++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
@@ -235,7 +235,7 @@ export interface ModuleFederationPluginOptions {
dataPrefetch?: DataPrefetch;
virtualRuntimeEntry?: boolean;
experiments?: {
- federationRuntime?: false | 'hoisted';
+ federationRuntime?: false | 'hoisted' | 'use-host';
};
}
/**
diff --git a/packages/webpack-bundler-runtime/package.json b/packages/webpack-bundler-runtime/package.json
index 56c579db08..212174e8e3 100644
--- a/packages/webpack-bundler-runtime/package.json
+++ b/packages/webpack-bundler-runtime/package.json
@@ -36,6 +36,10 @@
"import": "./dist/container.esm.js",
"require": "./dist/container.cjs.js"
},
+ "./embedded": {
+ "import": "./dist/embedded.esm.js",
+ "require": "./dist/embedded.cjs.js"
+ },
"./*": "./*"
},
"typesVersions": {
@@ -45,6 +49,9 @@
],
"constant": [
"./dist/constant.cjs.d.ts"
+ ],
+ "embedded": [
+ "./dist/embedded.cjs.d.ts"
]
}
},
diff --git a/packages/webpack-bundler-runtime/project.json b/packages/webpack-bundler-runtime/project.json
index 18606b0fd7..55b66502ec 100644
--- a/packages/webpack-bundler-runtime/project.json
+++ b/packages/webpack-bundler-runtime/project.json
@@ -19,7 +19,8 @@
"format": ["cjs", "esm"],
"additionalEntryPoints": [
"packages/webpack-bundler-runtime/src/constant.ts",
- "packages/webpack-bundler-runtime/src/container.ts"
+ "packages/webpack-bundler-runtime/src/container.ts",
+ "packages/webpack-bundler-runtime/src/embedded.ts"
],
"rollupConfig": "packages/webpack-bundler-runtime/rollup.config.js"
},
diff --git a/packages/webpack-bundler-runtime/src/embedded.ts b/packages/webpack-bundler-runtime/src/embedded.ts
new file mode 100644
index 0000000000..499849a6a6
--- /dev/null
+++ b/packages/webpack-bundler-runtime/src/embedded.ts
@@ -0,0 +1,87 @@
+import { Federation } from './types';
+import { remotes } from './remotes';
+import { consumes } from './consumes';
+import { initializeSharing } from './initializeSharing';
+import { installInitialConsumes } from './installInitialConsumes';
+import { attachShareScopeMap } from './attachShareScopeMap';
+import { initContainerEntry } from './initContainerEntry';
+export * from './types';
+
+// Ensure nativeGlobal is defined correctly
+export const nativeGlobal: typeof global = (() => {
+ try {
+ return new Function('return this')();
+ } catch {
+ return globalThis;
+ }
+})() as typeof global;
+
+// Safely access the shared runtime
+const sharedRuntime = nativeGlobal.__FEDERATION__?.__SHAREABLE_RUNTIME__;
+
+if (!sharedRuntime) {
+ throw new Error('Shared runtime is not available.');
+}
+
+// Create a new instance of FederationManager, handling the build identifier
+const federationInstance = new sharedRuntime.FederationManager(
+ //@ts-ignore
+ typeof FEDERATION_BUILD_IDENTIFIER === 'undefined'
+ ? undefined
+ : //@ts-ignore
+ FEDERATION_BUILD_IDENTIFIER,
+);
+
+// Bind methods of federationInstance to ensure correct `this` context
+// Without using destructuring or arrow functions
+const boundInit = federationInstance.init.bind(federationInstance);
+const boundGetInstance =
+ federationInstance.getInstance.bind(federationInstance);
+const boundLoadRemote = federationInstance.loadRemote.bind(federationInstance);
+const boundLoadShare = federationInstance.loadShare.bind(federationInstance);
+const boundLoadShareSync =
+ federationInstance.loadShareSync.bind(federationInstance);
+const boundPreloadRemote =
+ federationInstance.preloadRemote.bind(federationInstance);
+const boundRegisterRemotes =
+ federationInstance.registerRemotes.bind(federationInstance);
+const boundRegisterPlugins =
+ federationInstance.registerPlugins.bind(federationInstance);
+
+// Assemble the federation object with bound methods
+const federation: Federation = {
+ runtime: {
+ // General exports safe to share
+ FederationHost: sharedRuntime.FederationHost,
+ registerGlobalPlugins: sharedRuntime.registerGlobalPlugins,
+ getRemoteEntry: sharedRuntime.getRemoteEntry,
+ getRemoteInfo: sharedRuntime.getRemoteInfo,
+ loadScript: sharedRuntime.loadScript,
+ loadScriptNode: sharedRuntime.loadScriptNode,
+ FederationManager: sharedRuntime.FederationManager,
+ Module: sharedRuntime.Module,
+ // Runtime instance-specific methods with correct `this` binding
+ init: boundInit,
+ getInstance: boundGetInstance,
+ loadRemote: boundLoadRemote,
+ loadShare: boundLoadShare,
+ loadShareSync: boundLoadShareSync,
+ preloadRemote: boundPreloadRemote,
+ registerRemotes: boundRegisterRemotes,
+ registerPlugins: boundRegisterPlugins,
+ },
+ instance: undefined,
+ initOptions: undefined,
+ bundlerRuntime: {
+ remotes: remotes,
+ consumes: consumes,
+ I: initializeSharing,
+ S: {},
+ installInitialConsumes: installInitialConsumes,
+ initContainerEntry: initContainerEntry,
+ },
+ attachShareScopeMap: attachShareScopeMap,
+ bundlerRuntimeOptions: {},
+};
+
+export default federation;