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;