diff --git a/.changeset/real-peas-compare.md b/.changeset/real-peas-compare.md new file mode 100644 index 00000000000..ab91ebd04fc --- /dev/null +++ b/.changeset/real-peas-compare.md @@ -0,0 +1,5 @@ +--- +'@module-federation/webpack-bundler-runtime': patch +--- + +fix(webpack-bundler-runtime): update bundler runtime options before calling function diff --git a/.changeset/tough-insects-fetch.md b/.changeset/tough-insects-fetch.md new file mode 100644 index 00000000000..6488497bb89 --- /dev/null +++ b/.changeset/tough-insects-fetch.md @@ -0,0 +1,6 @@ +--- +'@module-federation/webpack-bundler-runtime': patch +'@module-federation/enhanced': patch +--- + +fix(webpack-bundler-runtime): align with rspack bundler runtime variable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69e4971e771..55d6976f7d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,6 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - cache: 'pnpm' # Update npm to the latest version to enable OIDC - name: Update npm @@ -64,7 +63,8 @@ jobs: - name: Generate preview version if: github.event.inputs.version == 'next' run: | - npx changeset version --snapshot next + SAFE_BRANCH=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]') + npx changeset version --snapshot "$SAFE_BRANCH" - name: Build and test Packages run: | diff --git a/packages/enhanced/jest.config.ts b/packages/enhanced/jest.config.ts index 4161f8ed279..e9f78245ad8 100644 --- a/packages/enhanced/jest.config.ts +++ b/packages/enhanced/jest.config.ts @@ -23,22 +23,36 @@ if (swcJestConfig.swcrc === undefined) { // jest needs EsModule Interop to find the default exported setup/teardown functions // swcJestConfig.module.noInterop = false; +const testMatch = []; + +if (process.env['TEST_TYPE'] === 'unit') { + testMatch.push('/test/unit/**/*.test.ts'); +} else { + testMatch.push('/test/*.basictest.js'); +} + export default { displayName: 'enhanced', preset: '../../jest.preset.js', - cacheDirectory: path.join(os.tmpdir(), 'enhanced'), + // Disable Jest's filesystem transform cache to avoid stale results + cache: false, + cacheDirectory: path.join( + os.tmpdir(), + process.env['TEST_TYPE'] || '', + 'enhanced', + ), transform: { '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../../coverage/packages/enhanced', rootDir: __dirname, - testMatch: [ - '/test/*.basictest.js', - '/test/unit/**/*.test.ts', - ], + testMatch, silent: true, verbose: false, + // Note: Do not enable `resetModules` here. Some unit tests rely on hoisted + // jest.mock() semantics across ESM/CJS boundaries, and forcing a registry + // reset can interfere with those mocks being applied at import time. testEnvironment: path.resolve(__dirname, './test/patch-node-env.js'), setupFilesAfterEnv: ['/test/setupTestFramework.js'], }; diff --git a/packages/enhanced/package.json b/packages/enhanced/package.json index 9797119ed30..c9cfbd8df74 100644 --- a/packages/enhanced/package.json +++ b/packages/enhanced/package.json @@ -87,7 +87,8 @@ "@types/btoa": "^1.2.5", "ajv": "^8.17.1", "enhanced-resolve": "^5.0.0", - "terser": "^5.37.0" + "terser": "^5.37.0", + "memfs": "4.46.0" }, "dependencies": { "@module-federation/bridge-react-webpack-plugin": "workspace:*", diff --git a/packages/enhanced/project.json b/packages/enhanced/project.json index 57375dba310..4730f4dbe0d 100644 --- a/packages/enhanced/project.json +++ b/packages/enhanced/project.json @@ -49,7 +49,11 @@ "parallel": false, "commands": [ { - "command": "node --expose-gc --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", + "command": "TEST_TYPE=basic node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", + "forwardAllArgs": false + }, + { + "command": "TEST_TYPE=unit node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", "forwardAllArgs": false } ] @@ -61,7 +65,7 @@ "parallel": false, "commands": [ { - "command": "node --expose-gc --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.embed.ts --silent", + "command": "node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.embed.ts --silent", "forwardAllArgs": false } ] diff --git a/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts b/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts index 2f50eca5a7e..d6a3b88b947 100644 --- a/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts +++ b/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts @@ -8,7 +8,10 @@ import RemoteModule from './RemoteModule'; import { getFederationGlobalScope } from './runtime/utils'; import type ExternalModule from 'webpack/lib/ExternalModule'; import type FallbackModule from './FallbackModule'; -import type { RemotesOptions } from '@module-federation/webpack-bundler-runtime'; +import type { + ModuleIdToRemoteDataMapping, + RemotesOptions, +} from '@module-federation/webpack-bundler-runtime'; const extractUrlAndGlobal = require( normalizeWebpackPath('webpack/lib/util/extractUrlAndGlobal'), @@ -32,6 +35,8 @@ class RemoteRuntimeModule extends RuntimeModule { const chunkToRemotesMapping: Record = {}; const idToExternalAndNameMapping: Record = {}; const idToRemoteMap: RemotesOptions['idToRemoteMap'] = {}; + const moduleIdToRemoteDataMapping: ModuleIdToRemoteDataMapping = {}; + // let chunkReferences: Set = new Set(); // if (this.chunk && chunkGraph) { @@ -113,8 +118,15 @@ class RemoteRuntimeModule extends RuntimeModule { idToRemoteMap[id].push({ externalType: remoteModule.externalType, name: remoteModule.externalType === 'script' ? remoteName : '', - externalModuleId, }); + moduleIdToRemoteDataMapping[id] = { + shareScope: shareScope as string, + name, + externalModuleId: externalModuleId as string, + // Preserve the extracted remote name so lazy updates can + // rebuild idToRemoteMap via updateRemoteOptions. + remoteName: remoteName, + }; }); } } @@ -135,7 +147,14 @@ class RemoteRuntimeModule extends RuntimeModule { '\t', )};`, `var idToRemoteMap = ${JSON.stringify(idToRemoteMap, null, '\t')};`, - `${federationGlobal}.bundlerRuntimeOptions.remotes = {idToRemoteMap,chunkMapping, idToExternalAndNameMapping, webpackRequire:${RuntimeGlobals.require}};`, + `${federationGlobal}.bundlerRuntimeOptions.remotes.chunkMapping = chunkMapping;`, + `${federationGlobal}.bundlerRuntimeOptions.remotes.idToExternalAndNameMapping = idToExternalAndNameMapping;`, + `${federationGlobal}.bundlerRuntimeOptions.remotes.idToRemoteMap = idToRemoteMap;`, + `${RuntimeGlobals.require}.remotesLoadingData.moduleIdToRemoteDataMapping = ${JSON.stringify( + moduleIdToRemoteDataMapping, + null, + '\t', + )};`, `${ RuntimeGlobals.ensureChunkHandlers }.remotes = ${runtimeTemplate.basicFunction('chunkId, promises', [ diff --git a/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts index 3e7eb12ea21..8bb09cda321 100644 --- a/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/FederationModulesPlugin.ts @@ -29,9 +29,20 @@ class FederationModulesPlugin { * @returns {CompilationHooks} the attached hooks */ static getCompilationHooks(compilation: CompilationType): CompilationHooks { - if (!(compilation instanceof Compilation)) { + // Avoid cross-realm instanceof checks (e.g., Jest VM modules) by using + // a duck-typed verification of a Webpack Compilation-like object. + const isLikelyCompilation = + compilation && + typeof compilation === 'object' && + // @ts-ignore + typeof (compilation as any).hooks === 'object' && + // A couple of well-known hooks available on Webpack 5 compilations + // @ts-ignore + typeof (compilation as any).hooks.processAssets?.tap === 'function'; + + if (!isLikelyCompilation) { throw new TypeError( - "The 'compilation' argument must be an instance of Compilation", + "Invalid 'compilation' argument: expected a Webpack Compilation-like object", ); } let hooks = compilationHooksMap.get(compilation); diff --git a/packages/enhanced/src/lib/container/runtime/getFederationGlobal.ts b/packages/enhanced/src/lib/container/runtime/getFederationGlobal.ts index 12d07593b3d..8b8d7dd41b7 100644 --- a/packages/enhanced/src/lib/container/runtime/getFederationGlobal.ts +++ b/packages/enhanced/src/lib/container/runtime/getFederationGlobal.ts @@ -2,6 +2,7 @@ import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-p import { getFederationGlobalScope } from './utils'; import type RuntimeGlobals from 'webpack/lib/RuntimeGlobals'; import { NormalizedRuntimeInitOptionsWithOutShared } from '../../../types/runtime'; +import type { RemoteInfos } from '@module-federation/webpack-bundler-runtime'; const { Template } = require( normalizeWebpackPath('webpack'), @@ -15,7 +16,29 @@ function getFederationGlobal( initOptionsWithoutShared: NormalizedRuntimeInitOptionsWithOutShared, ): string { const federationGlobal = getFederationGlobalScope(runtimeGlobals); - const initOptionsStrWithoutShared = JSON.stringify(initOptionsWithoutShared); + const initOptionsStrWithoutShared = JSON.stringify({ + ...initOptionsWithoutShared, + remotes: initOptionsWithoutShared.remotes.filter( + (remote) => remote.externalType === 'script', + ), + }); + const remoteInfos = JSON.stringify( + initOptionsWithoutShared.remotes.reduce((acc, remote) => { + const item: RemoteInfos[string][0] = { + alias: remote.alias || '', + name: remote.name, + // @ts-ignore + entry: remote.entry || '', + // @ts-ignore + shareScope: remote.shareScope, + externalType: remote.externalType, + }; + const key = remote.name || remote.alias || ''; + acc[key] ||= []; + acc[key].push(item); + return acc; + }, {} as RemoteInfos), + ); return template.asString([ `if(!${federationGlobal}){`, @@ -25,11 +48,12 @@ function getFederationGlobal( `initOptions: ${initOptionsStrWithoutShared},`, `chunkMatcher: function(chunkId) {return ${matcher}},`, `rootOutputDir: ${JSON.stringify(rootOutputDir || '')},`, - `initialConsumes: undefined,`, - 'bundlerRuntimeOptions: {}', + `bundlerRuntimeOptions: { remotes: { remoteInfos: ${remoteInfos}, webpackRequire: ${runtimeGlobals.require},idToRemoteMap: {}, chunkMapping: {},idToExternalAndNameMapping: {} } }`, ]), '};', ]), + `${runtimeGlobals.require}.consumesLoadingData = {}`, + `${runtimeGlobals.require}.remotesLoadingData = {}`, '}', ]); } diff --git a/packages/enhanced/src/lib/container/runtime/utils.ts b/packages/enhanced/src/lib/container/runtime/utils.ts index 18886f0ff01..44b9db33944 100644 --- a/packages/enhanced/src/lib/container/runtime/utils.ts +++ b/packages/enhanced/src/lib/container/runtime/utils.ts @@ -7,7 +7,6 @@ import upath from 'upath'; import path from 'path'; import crypto from 'crypto'; import { parseOptions } from '../options'; -import type { init } from '@module-federation/runtime-tools'; import type webpack from 'webpack'; import type RuntimeGlobals from 'webpack/lib/RuntimeGlobals'; import type { moduleFederationPlugin } from '@module-federation/sdk'; @@ -21,8 +20,6 @@ type EntryStaticNormalized = Awaited< ReturnType any>> >; -type Remotes = Parameters[0]['remotes']; - interface ModifyEntryOptions { compiler: webpack.Compiler; prependEntry?: (entry: EntryStaticNormalized) => void; @@ -49,27 +46,47 @@ export function normalizeRuntimeInitOptionsWithOutShared( shareScope: item.shareScope || options.shareScope || 'default', }), ); - const remoteOptions: Remotes = []; + const remoteOptions: NormalizedRuntimeInitOptionsWithOutShared['remotes'] = + []; parsedOptions.forEach((parsedOption) => { const [alias, remoteInfos] = parsedOption; const { external, shareScope } = remoteInfos; - try { - // only fit for remoteType: 'script' - const entry = external[0]; - if (/\s/.test(entry)) { + external.forEach((externalItem) => { + try { + const entry = externalItem; + if (/\s/.test(entry)) { + return; + } + const [url, globalName] = extractUrlAndGlobal(externalItem); + remoteOptions.push({ + alias, + name: globalName, + entry: url, + shareScope: shareScope, + externalType: 'script', + }); + } catch (err) { + const getExternalTypeFromExternal = (external: string) => { + if (/^[a-z0-9-]+ /.test(external)) { + const idx = external.indexOf(' '); + return [ + external.slice(0, idx) as moduleFederationPlugin.ExternalsType, + external.slice(idx + 1), + ] as const; + } + return null; + }; + remoteOptions.push({ + alias, + name: '', + entry: '', + shareScope: shareScope, + // @ts-ignore + externalType: getExternalTypeFromExternal(externalItem) || 'unknown', + }); return; } - const [url, globalName] = extractUrlAndGlobal(external[0]); - remoteOptions.push({ - alias, - name: globalName, - entry: url, - shareScope: shareScope, - // externalType - }); - } catch (err) { - return; - } + }); }); const initOptionsWithoutShared = { diff --git a/packages/enhanced/src/lib/sharing/ConsumeSharedRuntimeModule.ts b/packages/enhanced/src/lib/sharing/ConsumeSharedRuntimeModule.ts index 09ec9a9f5d1..87080db82c8 100644 --- a/packages/enhanced/src/lib/sharing/ConsumeSharedRuntimeModule.ts +++ b/packages/enhanced/src/lib/sharing/ConsumeSharedRuntimeModule.ts @@ -61,38 +61,30 @@ class ConsumeSharedRuntimeModule extends RuntimeModule { chunk.runtime, 'consume-shared', ); - const sharedInfoAndHandlerStr = Template.asString([ - '{', - Template.indent([ - `getter: ${moduleGetter.source().toString()},`, - `shareInfo: {`, + moduleIdToSourceMapping.set( + id, + Template.asString([ + '{', Template.indent([ - `shareConfig: ${JSON.stringify( - shareOption.shareConfig, - null, - 2, - )},`, - `scope: ${JSON.stringify( + `fallback: ${moduleGetter.source().toString()},`, + `shareScope: ${JSON.stringify( Array.isArray(shareOption.shareScope) ? shareOption.shareScope : [shareOption.shareScope || 'default'], )},`, + `singleton: ${JSON.stringify(shareOption.shareConfig.singleton)},`, + `requiredVersion: ${JSON.stringify(shareOption.shareConfig.requiredVersion)},`, + `strictVersion: ${JSON.stringify(shareOption.shareConfig.strictVersion)},`, + `eager: ${JSON.stringify(shareOption.shareConfig.eager)},`, + `layer: ${JSON.stringify(shareOption.shareConfig.layer)},`, + `shareKey: "${shareOption.shareKey}",`, ]), - '},', - `shareKey: "${shareOption.shareKey}",`, + '}', ]), - '}', - ]); - moduleIdToSourceMapping.set(id, sharedInfoAndHandlerStr); + ); } }; - // const chunkReferences = this._runtimeRequirements.has( - // 'federation-entry-startup', - // ) - // ? this.chunk?.getAllReferencedChunks() - // : this.chunk?.getAllAsyncChunks(); - // - // const allChunks = chunkReferences || []; + const allChunks = [...(this.chunk?.getAllReferencedChunks() || [])]; for (const chunk of allChunks) { const modules = chunkGraph.getChunkModulesIterableBySourceType( @@ -125,7 +117,7 @@ class ConsumeSharedRuntimeModule extends RuntimeModule { return Template.asString([ 'var installedModules = {};', - 'var moduleToHandlerMapping = {', + `${RuntimeGlobals.require}.consumesLoadingData.moduleIdToConsumeDataMapping = {`, Template.indent( Array.from(moduleIdToSourceMapping, ([key, value]) => { return `${JSON.stringify(key)}: ${value}`; @@ -133,16 +125,18 @@ class ConsumeSharedRuntimeModule extends RuntimeModule { ), '};', + `var moduleToHandlerMapping = {};`, + initialConsumes.length > 0 ? Template.asString([ - `var initialConsumes = ${JSON.stringify(initialConsumes)};`, + `${RuntimeGlobals.require}.consumesLoadingData.initialConsumes = ${JSON.stringify(initialConsumes)};`, `${federationGlobal}.installInitialConsumes = ${runtimeTemplate.returningFunction( Template.asString([ `${federationGlobal}.bundlerRuntime.installInitialConsumes({`, Template.indent([ - 'initialConsumes: initialConsumes,', + `initialConsumes: ${RuntimeGlobals.require}.consumesLoadingData.initialConsumes,`, 'installedModules:installedModules,', - 'moduleToHandlerMapping:moduleToHandlerMapping,', + `moduleToHandlerMapping,`, `webpackRequire: ${RuntimeGlobals.require}`, ]), `})`, @@ -153,7 +147,7 @@ class ConsumeSharedRuntimeModule extends RuntimeModule { : '// no consumes in initial chunks', this._runtimeRequirements.has(RuntimeGlobals.ensureChunkHandlers) ? Template.asString([ - `var chunkMapping = ${JSON.stringify( + `${RuntimeGlobals.require}.consumesLoadingData.chunkMapping = ${JSON.stringify( chunkToModuleMapping, null, '\t', @@ -162,10 +156,10 @@ class ConsumeSharedRuntimeModule extends RuntimeModule { RuntimeGlobals.ensureChunkHandlers }.consumes = ${runtimeTemplate.basicFunction('chunkId, promises', [ `${federationGlobal}.bundlerRuntime.consumes({`, - 'chunkMapping: chunkMapping,', + `chunkMapping: ${RuntimeGlobals.require}.consumesLoadingData.chunkMapping,`, 'installedModules: installedModules,', 'chunkId: chunkId,', - 'moduleToHandlerMapping: moduleToHandlerMapping,', + `moduleToHandlerMapping,`, 'promises: promises,', `webpackRequire:${RuntimeGlobals.require}`, '});', diff --git a/packages/enhanced/src/types/runtime.ts b/packages/enhanced/src/types/runtime.ts index 5d5f14bf50c..7d9e8c9b758 100644 --- a/packages/enhanced/src/types/runtime.ts +++ b/packages/enhanced/src/types/runtime.ts @@ -1,8 +1,11 @@ import type { init } from '@module-federation/runtime-tools'; +import type { moduleFederationPlugin } from '@module-federation/sdk'; type Remotes = Parameters[0]['remotes']; export interface NormalizedRuntimeInitOptionsWithOutShared { name: string; - remotes: Remotes; + remotes: Array< + Remotes[0] & { externalType: moduleFederationPlugin.ExternalsType } + >; } diff --git a/packages/enhanced/test/unit/container/FederationModulesPlugin.getCompilationHooks.test.ts b/packages/enhanced/test/unit/container/FederationModulesPlugin.getCompilationHooks.test.ts new file mode 100644 index 00000000000..d207ae46029 --- /dev/null +++ b/packages/enhanced/test/unit/container/FederationModulesPlugin.getCompilationHooks.test.ts @@ -0,0 +1,39 @@ +/* + * @jest-environment node + */ + +// Make normalizeWebpackPath identity so our virtual mocks resolve +jest.mock('@module-federation/sdk/normalize-webpack-path', () => ({ + normalizeWebpackPath: jest.fn((p: string) => p), +})); + +// Provide a virtual Compilation module so the import in the SUT doesn't throw +jest.mock('webpack/lib/Compilation', () => ({}), { virtual: true }); + +const FederationModulesPlugin = + require('../../../src/lib/container/runtime/FederationModulesPlugin').default; + +describe('FederationModulesPlugin.getCompilationHooks', () => { + it('returns stable hooks for a Compilation-like object', () => { + const compilation = { + hooks: { + processAssets: { tap: jest.fn() }, + }, + } as any; + + const hooks1 = FederationModulesPlugin.getCompilationHooks(compilation); + const hooks2 = FederationModulesPlugin.getCompilationHooks(compilation); + + expect(hooks1).toBe(hooks2); + expect(typeof hooks1.addContainerEntryDependency.tap).toBe('function'); + expect(typeof hooks1.addFederationRuntimeDependency.tap).toBe('function'); + expect(typeof hooks1.addRemoteDependency.tap).toBe('function'); + }); + + it('throws TypeError for invalid compilation shape', () => { + const badCompilation = { hooks: {} } as any; + expect(() => + FederationModulesPlugin.getCompilationHooks(badCompilation), + ).toThrow(TypeError); + }); +}); diff --git a/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts b/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts index 9910694ae53..cb548197d3b 100644 --- a/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts +++ b/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts @@ -169,7 +169,10 @@ describe('RemoteRuntimeModule', () => { 'var chunkMapping = {};', 'var idToExternalAndNameMapping = {};', 'var idToRemoteMap = {};', - '__FEDERATION__.bundlerRuntimeOptions.remotes = {idToRemoteMap,chunkMapping, idToExternalAndNameMapping, webpackRequire:__webpack_require__};', + '__FEDERATION__.bundlerRuntimeOptions.remotes.chunkMapping = chunkMapping;', + '__FEDERATION__.bundlerRuntimeOptions.remotes.idToExternalAndNameMapping = idToExternalAndNameMapping;', + '__FEDERATION__.bundlerRuntimeOptions.remotes.idToRemoteMap = idToRemoteMap;', + '__webpack_require__.remotesLoadingData.moduleIdToRemoteDataMapping = {};', '__webpack_require__.e.remotes = function(chunkId, promises) { __FEDERATION__.bundlerRuntime.remotes({idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire:__webpack_require__}); }', ].join('\n'); expect(normalized).toBe(expected); @@ -274,6 +277,10 @@ describe('RemoteRuntimeModule', () => { // Verify federation global scope is used expect(result).toContain('__FEDERATION__.bundlerRuntimeOptions.remotes'); expect(result).toContain('__FEDERATION__.bundlerRuntime.remotes'); + + // Also ensure moduleIdToRemoteDataMapping preserves remoteName + expect(result).toContain('moduleIdToRemoteDataMapping'); + expect(result).toContain('"remoteName": "app1"'); }); it('should handle fallback modules with requests', () => { diff --git a/packages/webpack-bundler-runtime/__tests__/updateOptions.test.ts b/packages/webpack-bundler-runtime/__tests__/updateOptions.test.ts new file mode 100644 index 00000000000..d2aee354d93 --- /dev/null +++ b/packages/webpack-bundler-runtime/__tests__/updateOptions.test.ts @@ -0,0 +1,442 @@ +import { + updateConsumeOptions, + updateRemoteOptions, +} from '../src/updateOptions'; +import { InstallInitialConsumesOptions, RemotesOptions } from '../src/types'; + +describe('updateOptions', () => { + describe('updateConsumeOptions', () => { + it('should update consume options with new data', () => { + const mockWebpackRequire = { + consumesLoadingData: { + moduleIdToConsumeDataMapping: { + module1: { shareScope: 'default', name: 'react' }, + module2: { shareScope: 'custom', name: 'lodash' }, + }, + initialConsumes: ['module1', 'module3'], + chunkMapping: { + chunk1: ['module1', 'module2'], + chunk2: ['module3'], + }, + }, + } as any; + + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: { + module3: { shareScope: 'existing', name: 'vue' } as any, + }, + installedModules: {}, + initialConsumes: ['module4'], + }; + + updateConsumeOptions(options); + + expect(options.moduleToHandlerMapping).toEqual({ + module3: { shareScope: 'existing', name: 'vue' } as any, + module1: { shareScope: 'default', name: 'react' } as any, + module2: { shareScope: 'custom', name: 'lodash' } as any, + }); + + expect(options.initialConsumes).toEqual([ + 'module4', + 'module1', + 'module3', + ]); + }); + + it('should handle empty consumesLoadingData', () => { + const mockWebpackRequire = {} as any; + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: {}, + installedModules: {}, + initialConsumes: [], + }; + + updateConsumeOptions(options); + + expect(options.moduleToHandlerMapping).toEqual({}); + expect(options.initialConsumes).toEqual([]); + }); + + it('should handle missing chunkMapping', () => { + const mockWebpackRequire = { + consumesLoadingData: { + moduleIdToConsumeDataMapping: { + module1: { shareScope: 'default', name: 'react' }, + }, + }, + } as any; + + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: {}, + installedModules: {}, + initialConsumes: [], + }; + + updateConsumeOptions(options); + + expect(options.moduleToHandlerMapping).toEqual({ + module1: { shareScope: 'default', name: 'react' } as any, + }); + }); + + it('should skip when _updated flag is set', () => { + const mockWebpackRequire = { + consumesLoadingData: { + _updated: 1, + moduleIdToConsumeDataMapping: { + module1: { shareScope: 'default', name: 'react' }, + }, + initialConsumes: ['module1'], + }, + } as any; + + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: {}, + installedModules: {}, + initialConsumes: [], + }; + + updateConsumeOptions(options); + + expect(options.moduleToHandlerMapping).toEqual({}); + expect(options.initialConsumes).toEqual([]); + }); + + // 新增测试:initializeSharingData 处理 + it('should handle initializeSharingData', () => { + const mockRegisterShared = jest.fn(); + const mockWebpackRequire = { + consumesLoadingData: {}, + initializeSharingData: { + scopeToSharingDataMapping: { + default: [ + { + name: 'react', + version: '18.0.0', + factory: () => ({}), + singleton: true, + eager: false, + }, + ], + }, + }, + federation: { + instance: { + registerShared: mockRegisterShared, + }, + }, + } as any; + + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: {}, + installedModules: {}, + initialConsumes: [], + }; + + updateConsumeOptions(options); + + expect(mockRegisterShared).toHaveBeenCalledWith({ + react: [ + { + version: '18.0.0', + scope: ['default'], + shareConfig: { + requiredVersion: '^18.0.0', + singleton: true, + eager: false, + }, + get: expect.any(Function), + }, + ], + }); + }); + + // 新增测试:initializeSharingData 跳过已更新 + it('should skip initializeSharingData when _updated flag is set', () => { + const mockRegisterShared = jest.fn(); + const mockWebpackRequire = { + initializeSharingData: { + _updated: 1, + scopeToSharingDataMapping: { + default: [{ name: 'react', version: '18.0.0' }], + }, + }, + federation: { + instance: { + registerShared: mockRegisterShared, + }, + }, + } as any; + + const options: InstallInitialConsumesOptions = { + webpackRequire: mockWebpackRequire, + moduleToHandlerMapping: {}, + installedModules: {}, + initialConsumes: [], + }; + + updateConsumeOptions(options); + + expect(mockRegisterShared).not.toHaveBeenCalled(); + }); + }); + + describe('updateRemoteOptions', () => { + it('should update remote options with new data', () => { + const mockWebpackRequire = { + remotesLoadingData: { + chunkMapping: { remote1: ['chunk1'] }, + moduleIdToRemoteDataMapping: { + remoteModule1: { + shareScope: 'default', + name: 'react', + remoteName: 'reactRemote', + externalModuleId: 'react-external', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + reactRemote: { + url: 'http://localhost:3001', + name: 'react', + } as any, + }, + }, + }, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + expect(options.idToExternalAndNameMapping).toEqual({ + remoteModule1: ['default', 'react', 'react-external'], + }); + + expect(options.idToRemoteMap).toEqual({ + remoteModule1: [{ url: 'http://localhost:3001', name: 'react' }], + }); + }); + + it('should handle missing remotesLoadingData', () => { + const mockWebpackRequire = {} as any; + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + expect(options.idToExternalAndNameMapping).toEqual({}); + expect(options.idToRemoteMap).toEqual({}); + }); + + it('should handle existing mappings', () => { + const mockWebpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + existing: { + shareScope: 'existing', + name: 'existing-name', + remoteName: 'existing-remote', + externalModuleId: 'existing-external', + }, + }, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: { + existing: ['old', 'old-name', 'old-external'], + }, + idToRemoteMap: { + existing: [{ url: 'old-url', name: 'old' } as any], + }, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + // 应该保留现有映射,不覆盖 + expect(options.idToExternalAndNameMapping['existing']).toEqual([ + 'old', + 'old-name', + 'old-external', + ]); + expect(options.idToRemoteMap['existing']).toEqual([ + { url: 'old-url', name: 'old' }, + ]); + }); + + it('should handle missing remoteInfos', () => { + const mockWebpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + remoteModule1: { + shareScope: 'default', + name: 'react', + remoteName: 'missingRemote', + externalModuleId: 'react-external', + }, + }, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + expect(options.idToExternalAndNameMapping).toEqual({ + remoteModule1: ['default', 'react', 'react-external'], + }); + expect(options.idToRemoteMap).toEqual({}); + }); + + // 新增测试:_updated 标志 + it('should skip when _updated flag is set', () => { + const mockWebpackRequire = { + remotesLoadingData: { + _updated: 1, + chunkMapping: { remote1: ['chunk1'] }, + moduleIdToRemoteDataMapping: { + remoteModule1: { + shareScope: 'default', + name: 'react', + remoteName: 'reactRemote', + externalModuleId: 'react-external', + }, + }, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + expect(options.idToExternalAndNameMapping).toEqual({}); + expect(options.idToRemoteMap).toEqual({}); + }); + + // 新增测试:chunkMapping 更新 + it('should update chunkMapping correctly', () => { + const mockWebpackRequire = { + remotesLoadingData: { + chunkMapping: { + chunk1: ['module1', 'module2'], + chunk2: ['module3'], + }, + moduleIdToRemoteDataMapping: {}, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: { + chunk3: ['module4'], + }, + }; + + updateRemoteOptions(options); + + expect(options.chunkMapping).toEqual({ + chunk3: ['module4'], + chunk1: ['module1', 'module2'], + chunk2: ['module3'], + }); + }); + + // 新增测试:多个远程项 + it('should handle multiple remote items', () => { + const mockWebpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + module1: { + shareScope: 'default', + name: 'react', + remoteName: 'reactRemote', + externalModuleId: 'react-external', + }, + module2: { + shareScope: 'custom', + name: 'lodash', + remoteName: 'lodashRemote', + externalModuleId: 'lodash-external', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + reactRemote: [ + { url: 'http://react.com', name: 'react' }, + ] as any, + lodashRemote: [ + { url: 'http://lodash.com', name: 'lodash' }, + ] as any, + }, + }, + }, + }, + } as any; + + const options: RemotesOptions = { + webpackRequire: mockWebpackRequire, + chunkId: 'test-chunk', + promises: [], + idToExternalAndNameMapping: {}, + idToRemoteMap: {}, + chunkMapping: {}, + }; + + updateRemoteOptions(options); + + expect(options.idToExternalAndNameMapping).toEqual({ + module1: ['default', 'react', 'react-external'], + module2: ['custom', 'lodash', 'lodash-external'], + }); + }); + }); +}); diff --git a/packages/webpack-bundler-runtime/src/consumes.ts b/packages/webpack-bundler-runtime/src/consumes.ts index e76fbe41833..2c0ad375b9d 100644 --- a/packages/webpack-bundler-runtime/src/consumes.ts +++ b/packages/webpack-bundler-runtime/src/consumes.ts @@ -1,15 +1,18 @@ import { ConsumesOptions } from './types'; import { attachShareScopeMap } from './attachShareScopeMap'; +import { updateConsumeOptions } from './updateOptions'; export function consumes(options: ConsumesOptions) { + updateConsumeOptions(options); const { chunkId, promises, - chunkMapping, installedModules, - moduleToHandlerMapping, webpackRequire, + chunkMapping, + moduleToHandlerMapping, } = options; + attachShareScopeMap(webpackRequire); if (webpackRequire.o(chunkMapping, chunkId)) { chunkMapping[chunkId].forEach((id) => { diff --git a/packages/webpack-bundler-runtime/src/container.ts b/packages/webpack-bundler-runtime/src/container.ts deleted file mode 100644 index a3fb08394e6..00000000000 --- a/packages/webpack-bundler-runtime/src/container.ts +++ /dev/null @@ -1,248 +0,0 @@ -// import bundler_runtime_base from './index'; -// import type { UserOptions } from '@module-federation/runtime/types'; - -// interface ExtendedOptions extends UserOptions { -// exposes: { [key: string]: () => Promise<() => any> }; -// } - -// export const createContainer = (federationOptions: ExtendedOptions) => { -// const { exposes, name, remotes = [], shared, plugins } = federationOptions; - -// const __webpack_modules__ = { -// './node_modules/.federation/entry.1f2288102e035e2ed66b2efaf60ad043.js': ( -// //@ts-ignore -// module, -// //@ts-ignore -// __webpack_exports__, -// //@ts-ignore -// __webpack_require__, -// ) => { -// __webpack_require__.r(__webpack_exports__); -// const bundler_runtime = __webpack_require__.n(bundler_runtime_base); -// const prevFederation = __webpack_require__.federation; -// __webpack_require__.federation = {}; -// for (const key in bundler_runtime()) { -// __webpack_require__.federation[key] = bundler_runtime()[key]; -// } -// for (const key in prevFederation) { -// __webpack_require__.federation[key] = prevFederation[key]; -// } -// if (!__webpack_require__.federation.instance) { -// const pluginsToAdd = plugins || []; -// __webpack_require__.federation.initOptions.plugins = __webpack_require__ -// .federation.initOptions.plugins -// ? __webpack_require__.federation.initOptions.plugins.concat( -// pluginsToAdd, -// ) -// : pluginsToAdd; -// __webpack_require__.federation.instance = -// __webpack_require__.federation.runtime.init( -// __webpack_require__.federation.initOptions, -// ); -// if (__webpack_require__.federation.attachShareScopeMap) { -// __webpack_require__.federation.attachShareScopeMap( -// __webpack_require__, -// ); -// } -// if (__webpack_require__.federation.installInitialConsumes) { -// __webpack_require__.federation.installInitialConsumes(); -// } -// } -// }, -// //@ts-ignore -// 'webpack/container/entry/createContainer': ( -// //@ts-ignore - -// module, -// //@ts-ignore -// exports, -// //@ts-ignore -// __webpack_require__, -// ) => { -// const moduleMap = {}; -// for (const key in exposes) { -// if (Object.prototype.hasOwnProperty.call(exposes, key)) { -// //@ts-ignore -// moduleMap[key] = () => -// Promise.resolve(exposes[key]()).then((m) => () => m); -// } -// } -// //@ts-ignore -// const get = (module, getScope) => { -// __webpack_require__.R = getScope; -// getScope = __webpack_require__.o(moduleMap, module) -// ? //@ts-ignore -// moduleMap[module]() -// : Promise.resolve().then(() => { -// throw new Error( -// `Module "${module}" does not exist in container.`, -// ); -// }); -// __webpack_require__.R = undefined; -// return getScope; -// }; -// //@ts-ignore -// const init = (shareScope, initScope, remoteEntryInitOptions) => { -// return __webpack_require__.federation.bundlerRuntime.initContainerEntry( -// { -// webpackRequire: __webpack_require__, -// shareScope: shareScope, -// initScope: initScope, -// remoteEntryInitOptions: remoteEntryInitOptions, -// shareScopeKey: 'default', -// }, -// ); -// }; -// __webpack_require__( -// './node_modules/.federation/entry.1f2288102e035e2ed66b2efaf60ad043.js', -// ); - -// // This exports getters to disallow modifications -// __webpack_require__.d(exports, { -// get: () => get, -// init: () => init, -// moduleMap: () => moduleMap, -// }); -// }, -// }; - -// const __webpack_module_cache__ = {}; - -// //@ts-ignore -// const __webpack_require__ = (moduleId) => { -// //@ts-ignore -// let cachedModule = __webpack_module_cache__[moduleId]; -// if (cachedModule !== undefined) { -// return cachedModule.exports; -// } -// //@ts-ignore -// let module = (__webpack_module_cache__[moduleId] = { -// id: moduleId, -// loaded: false, -// exports: {}, -// }); - -// const execOptions = { -// id: moduleId, -// module: module, -// //@ts-ignore -// factory: __webpack_modules__[moduleId], -// require: __webpack_require__, -// }; -// __webpack_require__.i.forEach((handler) => { -// handler(execOptions); -// }); -// module = execOptions.module; -// execOptions.factory.call( -// module.exports, -// module, -// module.exports, -// execOptions.require, -// ); - -// module.loaded = true; - -// return module.exports; -// }; - -// __webpack_require__.m = __webpack_modules__; -// __webpack_require__.c = __webpack_module_cache__; -// //@ts-ignore -// __webpack_require__.i = []; - -// //@ts-ignore -// if (!__webpack_require__.federation) { -// __webpack_require__.federation = { -// initOptions: { -// name: name, -// //@ts-ignore -// remotes: remotes.map((remote) => ({ -// type: remote.type, -// alias: remote.alias, -// name: remote.name, -// //@ts-ignore -// entry: remote.entry, -// shareScope: remote.shareScope || 'default', -// })), -// }, -// chunkMatcher: () => true, -// rootOutputDir: '', -// initialConsumes: undefined, -// bundlerRuntimeOptions: {}, -// }; -// } -// //@ts-ignore -// __webpack_require__.n = (module) => { -// const getter = -// module && module.__esModule ? () => module['default'] : () => module; -// __webpack_require__.d(getter, { a: getter }); -// return getter; -// }; - -// //@ts-ignore -// __webpack_require__.d = (exports, definition) => { -// for (const key in definition) { -// if ( -// __webpack_require__.o(definition, key) && -// !__webpack_require__.o(exports, key) -// ) { -// Object.defineProperty(exports, key, { -// enumerable: true, -// get: definition[key], -// }); -// } -// } -// }; - -// __webpack_require__.f = {}; - -// __webpack_require__.g = (() => { -// if (typeof globalThis === 'object') return globalThis; -// try { -// return this || new Function('return this')(); -// } catch (e) { -// if (typeof window === 'object') return window; -// } -// })(); - -// //@ts-ignore -// __webpack_require__.o = (obj, prop) => -// Object.prototype.hasOwnProperty.call(obj, prop); -// //@ts-ignore -// __webpack_require__.r = (exports) => { -// if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { -// Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -// } -// Object.defineProperty(exports, '__esModule', { value: true }); -// }; - -// //@ts-ignore -// __webpack_require__.federation.initOptions.shared = shared; -// __webpack_require__.S = {}; -// const initPromises = {}; -// const initTokens = {}; -// //@ts-ignore -// __webpack_require__.I = (name, initScope) => { -// //@ts-ignore -// return __webpack_require__.federation.bundlerRuntime.I({ -// shareScopeName: name, -// webpackRequire: __webpack_require__, -// initPromises: initPromises, -// initTokens: initTokens, -// initScope: initScope, -// }); -// }; - -// const __webpack_exports__ = __webpack_require__( -// 'webpack/container/entry/createContainer', -// ); -// const __webpack_exports__get = __webpack_exports__.get; -// const __webpack_exports__init = __webpack_exports__.init; -// return __webpack_exports__; -// }; -// export const createContainerAsync = async ( -// federationOptions: ExtendedOptions, -// ) => { -// // todo: consider async startup options here, for "async boundary" provision. -// return createContainer(federationOptions); -// }; diff --git a/packages/webpack-bundler-runtime/src/installInitialConsumes.ts b/packages/webpack-bundler-runtime/src/installInitialConsumes.ts index f6cc3aa2077..fc4c0eb92c6 100644 --- a/packages/webpack-bundler-runtime/src/installInitialConsumes.ts +++ b/packages/webpack-bundler-runtime/src/installInitialConsumes.ts @@ -2,6 +2,7 @@ import { HandleInitialConsumesOptions, InstallInitialConsumesOptions, } from './types'; +import { updateConsumeOptions } from './updateOptions'; function handleInitialConsumes(options: HandleInitialConsumesOptions) { const { moduleId, moduleToHandlerMapping, webpackRequire } = options; @@ -25,12 +26,10 @@ function handleInitialConsumes(options: HandleInitialConsumesOptions) { } export function installInitialConsumes(options: InstallInitialConsumesOptions) { - const { - moduleToHandlerMapping, - webpackRequire, - installedModules, - initialConsumes, - } = options; + const { webpackRequire } = options; + + updateConsumeOptions(options); + const { initialConsumes, moduleToHandlerMapping, installedModules } = options; initialConsumes.forEach((id) => { webpackRequire.m[id] = (module) => { diff --git a/packages/webpack-bundler-runtime/src/remotes.ts b/packages/webpack-bundler-runtime/src/remotes.ts index 7871d39011a..ed753a6b880 100644 --- a/packages/webpack-bundler-runtime/src/remotes.ts +++ b/packages/webpack-bundler-runtime/src/remotes.ts @@ -3,14 +3,17 @@ import type { RemoteEntryExports } from './types'; import { RemotesOptions } from './types'; import { FEDERATION_SUPPORTED_TYPES } from './constant'; import { decodeName, ENCODE_NAME_PREFIX } from '@module-federation/sdk'; +import { updateRemoteOptions } from './updateOptions'; export function remotes(options: RemotesOptions) { + updateRemoteOptions(options); + const { chunkId, promises, + webpackRequire, chunkMapping, idToExternalAndNameMapping, - webpackRequire, idToRemoteMap, } = options; attachShareScopeMap(webpackRequire); @@ -22,7 +25,7 @@ export function remotes(options: RemotesOptions) { getScope = []; } const data = idToExternalAndNameMapping[id]; - const remoteInfos = idToRemoteMap[id]; + const remoteInfos = idToRemoteMap[id] || []; // @ts-ignore seems not work if (getScope.indexOf(data) >= 0) { return; diff --git a/packages/webpack-bundler-runtime/src/types.ts b/packages/webpack-bundler-runtime/src/types.ts index 4905a1806b9..64e19b06bb6 100644 --- a/packages/webpack-bundler-runtime/src/types.ts +++ b/packages/webpack-bundler-runtime/src/types.ts @@ -1,11 +1,14 @@ import * as runtime from '@module-federation/runtime'; import type { + Remote, RemoteEntryInitOptions, SharedConfig, + SharedGetter, } from '@module-federation/runtime/types'; import { initializeSharing } from './initializeSharing'; import { attachShareScopeMap } from './attachShareScopeMap'; import { initContainerEntry } from './initContainerEntry'; +import type { moduleFederationPlugin } from '@module-federation/sdk'; // FIXME: ideal situation => import { GlobalShareScope,UserOptions } from '@module-federation/runtime/types' type ExcludeUndefined = T extends undefined ? never : T; @@ -13,7 +16,11 @@ type Shared = InitOptions['shared']; type NonUndefined = ExcludeUndefined; -type InitOptions = Parameters[0]; +type InitOptions = Omit[0], 'remotes'> & { + remotes: Array< + Remote & { externalType: moduleFederationPlugin.ExternalsType } + >; +}; type ModuleCache = runtime.ModuleFederation['moduleCache']; type InferModule = T extends Map ? U : never; @@ -47,11 +54,58 @@ type InferredGlobalShareScope = { // shareScope, name, externalModuleId type IdToExternalAndNameMappingItem = [string, string, string | number]; - interface IdToExternalAndNameMappingItemWithPromise extends IdToExternalAndNameMappingItem { p?: Promise | number; } +export type IdToExternalAndNameMapping = Record< + string, + IdToExternalAndNameMappingItemWithPromise +>; + +export type ModuleId = string | number; + +export type RemoteDataItem = { + shareScope: string; + name: string; + externalModuleId: ModuleId; + remoteName: string; +}; +export type ModuleIdToRemoteDataMapping = Record; + +type ModuleIdToConsumeDataMapping = { + fallback: () => Promise; + shareKey: string; + shareScope: string | string[]; +} & SharedConfig; +type WithStatus = T & { _updated: number }; +// It will update while lazy compile +export type ConsumesLoadingData = WithStatus<{ + chunkMapping?: Record>; + moduleIdToConsumeDataMapping?: Record; + initialConsumes?: Array; +}>; + +// It will update while lazy compile +export type RemotesLoadingData = WithStatus<{ + chunkMapping?: Record>; + moduleIdToRemoteDataMapping?: ModuleIdToRemoteDataMapping; +}>; + +export type InitializeSharingData = WithStatus<{ + scopeToSharingDataMapping: { + [shareScope: string]: Array<{ + name: string; + version: string; + factory: SharedGetter; + eager?: boolean; + singleton?: boolean; + requiredVersion?: string; + strictVersion?: boolean; + }>; + }; + uniqueName: string; +}>; export interface WebpackRequire { (moduleId: string | number): any; @@ -66,6 +120,9 @@ export interface WebpackRequire { ) => ReturnType; S?: InferredGlobalShareScope; federation: Federation; + consumesLoadingData?: ConsumesLoadingData; + remotesLoadingData?: RemotesLoadingData; + initializeSharingData?: InitializeSharingData; } interface ShareInfo { @@ -80,23 +137,36 @@ interface ModuleToHandlerMappingItem { shareKey: string; } -interface IdToRemoteMapItem { +export interface IdToRemoteMapItem { externalType: string; name: string; - externalModuleId?: string | number; } -export interface RemotesOptions { +export type IdToRemoteMap = Record; + +export type RemoteInfos = Record< + string, + Array< + IdToRemoteMapItem & { + alias: string; + entry?: string; + shareScope: string; + } + > +>; +export type RemoteChunkMapping = Record>; + +export type CoreRemotesOptions = { + idToRemoteMap: IdToRemoteMap; + chunkMapping: RemoteChunkMapping; + idToExternalAndNameMapping: IdToExternalAndNameMapping; +}; + +export type RemotesOptions = { chunkId: string | number; promises: Promise[]; - chunkMapping: Record>; - idToExternalAndNameMapping: Record< - string, - IdToExternalAndNameMappingItemWithPromise - >; - idToRemoteMap: Record; webpackRequire: WebpackRequire; -} +} & CoreRemotesOptions; export interface HandleInitialConsumesOptions { moduleId: string | number; @@ -140,7 +210,9 @@ export interface Federation { initContainerEntry: typeof initContainerEntry; }; bundlerRuntimeOptions: { - remotes?: Exclude; + remotes?: Exclude & { + remoteInfos?: RemoteInfos; + }; }; attachShareScopeMap?: typeof attachShareScopeMap; hasAttachShareScopeMap?: boolean; diff --git a/packages/webpack-bundler-runtime/src/updateOptions.ts b/packages/webpack-bundler-runtime/src/updateOptions.ts new file mode 100644 index 00000000000..2d0fa498a85 --- /dev/null +++ b/packages/webpack-bundler-runtime/src/updateOptions.ts @@ -0,0 +1,185 @@ +import type { + IdToRemoteMapItem, + RemotesOptions, + InstallInitialConsumesOptions, + ConsumesOptions, +} from './types'; +import type { + UserOptions, + ShareArgs, + SharedConfig, +} from '@module-federation/runtime/types'; + +export function updateConsumeOptions( + options: InstallInitialConsumesOptions | ConsumesOptions, +) { + const { webpackRequire, moduleToHandlerMapping } = options; + const { consumesLoadingData, initializeSharingData } = webpackRequire; + if (consumesLoadingData && !consumesLoadingData._updated) { + const { + moduleIdToConsumeDataMapping: updatedModuleIdToConsumeDataMapping = {}, + initialConsumes: updatedInitialConsumes = [], + chunkMapping: updatedChunkMapping = {}, + } = consumesLoadingData; + + Object.entries(updatedModuleIdToConsumeDataMapping).forEach( + ([id, data]) => { + if (!moduleToHandlerMapping[id]) { + moduleToHandlerMapping[id] = { + getter: data.fallback, + shareInfo: { + shareConfig: { + requiredVersion: data.requiredVersion, + strictVersion: data.strictVersion, + singleton: data.singleton, + eager: data.eager, + layer: data.layer, + }, + scope: Array.isArray(data.shareScope) + ? data.shareScope + : [data.shareScope || 'default'], + }, + shareKey: data.shareKey, + }; + } + }, + ); + + if ('initialConsumes' in options) { + const { initialConsumes = [] } = options; + updatedInitialConsumes.forEach((id) => { + if (!initialConsumes.includes(id)) { + initialConsumes.push(id); + } + }); + } + + if ('chunkMapping' in options) { + const { chunkMapping = {} } = options; + Object.entries(updatedChunkMapping).forEach(([id, chunkModules]) => { + if (!chunkMapping[id]) { + chunkMapping[id] = []; + } + chunkModules.forEach((moduleId) => { + if (!chunkMapping[id].includes(moduleId)) { + chunkMapping[id].push(moduleId); + } + }); + }); + } + consumesLoadingData._updated = 1; + } + + if (initializeSharingData && !initializeSharingData._updated) { + const { federation } = webpackRequire; + if ( + !federation.instance || + !initializeSharingData.scopeToSharingDataMapping + ) { + return; + } + const shared: Record> = {}; + for (let [scope, stages] of Object.entries( + initializeSharingData.scopeToSharingDataMapping, + )) { + for (let stage of stages) { + if (typeof stage === 'object' && stage !== null) { + const { + name, + version, + factory, + eager, + singleton, + requiredVersion, + strictVersion, + } = stage; + const shareConfig: SharedConfig = { + requiredVersion: `^${version}`, + }; + const isValidValue = function ( + val: unknown, + ): val is NonNullable { + return typeof val !== 'undefined'; + }; + if (isValidValue(singleton)) { + shareConfig.singleton = singleton; + } + if (isValidValue(requiredVersion)) { + shareConfig.requiredVersion = requiredVersion; + } + if (isValidValue(eager)) { + shareConfig.eager = eager; + } + if (isValidValue(strictVersion)) { + shareConfig.strictVersion = strictVersion; + } + const options = { + version, + scope: [scope], + shareConfig, + get: factory, + }; + if (shared[name]) { + shared[name].push(options); + } else { + shared[name] = [options]; + } + } + } + } + federation.instance.registerShared(shared); + initializeSharingData._updated = 1; + } +} + +export function updateRemoteOptions(options: RemotesOptions) { + const { + webpackRequire, + idToExternalAndNameMapping = {}, + idToRemoteMap = {}, + chunkMapping = {}, + } = options; + const { remotesLoadingData } = webpackRequire; + const remoteInfos = + webpackRequire.federation?.bundlerRuntimeOptions?.remotes?.remoteInfos; + if (!remotesLoadingData || remotesLoadingData._updated || !remoteInfos) { + return; + } + const { chunkMapping: updatedChunkMapping, moduleIdToRemoteDataMapping } = + remotesLoadingData; + if (!updatedChunkMapping || !moduleIdToRemoteDataMapping) { + return; + } + for (let [moduleId, data] of Object.entries(moduleIdToRemoteDataMapping)) { + if (!idToExternalAndNameMapping[moduleId]) { + idToExternalAndNameMapping[moduleId] = [ + data.shareScope, + data.name, + data.externalModuleId, + ]; + } + if (!idToRemoteMap[moduleId] && remoteInfos[data.remoteName]) { + const items = remoteInfos[data.remoteName]; + idToRemoteMap[moduleId] ||= []; + items.forEach((item) => { + if (!idToRemoteMap[moduleId].includes(item)) { + idToRemoteMap[moduleId].push(item); + } + }); + } + } + + if (chunkMapping) { + Object.entries(updatedChunkMapping).forEach(([id, chunkModules]) => { + if (!chunkMapping[id]) { + chunkMapping[id] = []; + } + chunkModules.forEach((moduleId) => { + if (!chunkMapping[id].includes(moduleId)) { + chunkMapping[id].push(moduleId); + } + }); + }); + } + remotesLoadingData._updated = 1; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 121d3171202..70d8a75c33d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3165,6 +3165,9 @@ importers: enhanced-resolve: specifier: ^5.0.0 version: 5.17.1 + memfs: + specifier: 4.46.0 + version: 4.46.0 terser: specifier: ^5.37.0 version: 5.37.0 @@ -9212,24 +9215,55 @@ packages: dependencies: tslib: 2.8.1 - /@jsonjoy.com/json-pack@1.1.0(tslib@2.8.1): - resolution: {integrity: sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==} + /@jsonjoy.com/buffers@1.0.0(tslib@2.8.1): + resolution: {integrity: sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + + /@jsonjoy.com/codegen@1.0.0(tslib@2.8.1): + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + + /@jsonjoy.com/json-pack@1.14.0(tslib@2.8.1): + resolution: {integrity: sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' dependencies: '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) - '@jsonjoy.com/util': 1.3.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) hyperdyperid: 1.2.0 - thingies: 1.21.0(tslib@2.8.1) + thingies: 2.5.0(tslib@2.8.1) + tslib: 2.8.1 + + /@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1): + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) tslib: 2.8.1 - /@jsonjoy.com/util@1.3.0(tslib@2.8.1): - resolution: {integrity: sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==} + /@jsonjoy.com/util@1.9.0(tslib@2.8.1): + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' dependencies: + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 /@juggle/resize-observer@3.4.0: @@ -32155,6 +32189,14 @@ packages: dependencies: is-glob: 4.0.3 + /glob-to-regex.js@1.0.1(tslib@2.8.1): + resolution: {integrity: sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -36409,23 +36451,14 @@ packages: dependencies: fs-monkey: 1.0.6 - /memfs@4.17.0: - resolution: {integrity: sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==} - engines: {node: '>= 4.0.0'} - dependencies: - '@jsonjoy.com/json-pack': 1.1.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.3.0(tslib@2.8.1) - tree-dump: 1.0.2(tslib@2.8.1) - tslib: 2.8.1 - dev: true - - /memfs@4.36.0: - resolution: {integrity: sha512-mfBfzGUdoEw5AZwG8E965ej3BbvW2F9LxEWj4uLxF6BEh1dO2N9eS3AGu9S6vfenuQYrVjsbUOOZK7y3vz4vyQ==} - engines: {node: '>= 4.0.0'} + /memfs@4.46.0: + resolution: {integrity: sha512-//IxqL9OO/WMpm2kE2aq+y7vO7/xS9xgVIbFM8RUIfW7TY7lowtnuS1j9MwLGm0OwcHUa4p8Bp+40W7f1BiWGQ==} dependencies: - '@jsonjoy.com/json-pack': 1.1.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.3.0(tslib@2.8.1) - tree-dump: 1.0.2(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.14.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.0.1(tslib@2.8.1) + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 /memoize-one@5.2.1: @@ -47347,8 +47380,8 @@ packages: dependencies: any-promise: 1.3.0 - /thingies@1.21.0(tslib@2.8.1): - resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + /thingies@2.5.0(tslib@2.8.1): + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} engines: {node: '>=10.18'} peerDependencies: tslib: ^2 @@ -47589,8 +47622,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /tree-dump@1.0.2(tslib@2.8.1): - resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + /tree-dump@1.1.0(tslib@2.8.1): + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -47662,7 +47695,7 @@ packages: '@rspack/lite-tapable': 1.0.1 chokidar: 3.6.0 is-glob: 4.0.3 - memfs: 4.17.0 + memfs: 4.46.0 minimatch: 9.0.5 picocolors: 1.1.1 typescript: 5.8.3 @@ -47683,7 +47716,7 @@ packages: '@rspack/lite-tapable': 1.0.1 chokidar: 3.6.0 is-glob: 4.0.3 - memfs: 4.36.0 + memfs: 4.46.0 minimatch: 9.0.5 picocolors: 1.1.1 typescript: 5.0.4 @@ -47704,7 +47737,7 @@ packages: '@rspack/lite-tapable': 1.0.1 chokidar: 3.6.0 is-glob: 4.0.3 - memfs: 4.36.0 + memfs: 4.46.0 minimatch: 9.0.5 picocolors: 1.1.1 typescript: 5.5.2 @@ -47725,7 +47758,7 @@ packages: '@rspack/lite-tapable': 1.0.1 chokidar: 3.6.0 is-glob: 4.0.3 - memfs: 4.36.0 + memfs: 4.46.0 minimatch: 9.0.5 picocolors: 1.1.1 typescript: 5.8.3 @@ -49976,7 +50009,7 @@ packages: optional: true dependencies: colorette: 2.0.20 - memfs: 4.36.0 + memfs: 4.46.0 mime-types: 2.1.35 on-finished: 2.4.1 range-parser: 1.2.1 @@ -49993,7 +50026,7 @@ packages: optional: true dependencies: colorette: 2.0.20 - memfs: 4.36.0 + memfs: 4.46.0 mime-types: 2.1.35 on-finished: 2.4.1 range-parser: 1.2.1