Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-peas-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/webpack-bundler-runtime': patch
---

fix(webpack-bundler-runtime): update bundler runtime options before calling function
6 changes: 6 additions & 0 deletions .changeset/tough-insects-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@module-federation/webpack-bundler-runtime': patch
'@module-federation/enhanced': patch
---

fix(webpack-bundler-runtime): align with rspack bundler runtime variable
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
24 changes: 19 additions & 5 deletions packages/enhanced/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<rootDir>/test/unit/**/*.test.ts');
} else {
testMatch.push('<rootDir>/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: [
'<rootDir>/test/*.basictest.js',
'<rootDir>/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: ['<rootDir>/test/setupTestFramework.js'],
};
3 changes: 2 additions & 1 deletion packages/enhanced/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
8 changes: 6 additions & 2 deletions packages/enhanced/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -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
}
]
Expand Down
25 changes: 22 additions & 3 deletions packages/enhanced/src/lib/container/RemoteRuntimeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -32,6 +35,8 @@ class RemoteRuntimeModule extends RuntimeModule {
const chunkToRemotesMapping: Record<string, any> = {};
const idToExternalAndNameMapping: Record<string | number, any> = {};
const idToRemoteMap: RemotesOptions['idToRemoteMap'] = {};
const moduleIdToRemoteDataMapping: ModuleIdToRemoteDataMapping = {};

// let chunkReferences: Set<Chunk> = new Set();

// if (this.chunk && chunkGraph) {
Expand Down Expand Up @@ -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,
};
});
}
}
Expand All @@ -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', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 27 additions & 3 deletions packages/enhanced/src/lib/container/runtime/getFederationGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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}){`,
Expand All @@ -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 = {}`,
'}',
]);
}
Expand Down
55 changes: 36 additions & 19 deletions packages/enhanced/src/lib/container/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,8 +20,6 @@ type EntryStaticNormalized = Awaited<
ReturnType<Extract<webpack.WebpackOptionsNormalized['entry'], () => any>>
>;

type Remotes = Parameters<typeof init>[0]['remotes'];

interface ModifyEntryOptions {
compiler: webpack.Compiler;
prependEntry?: (entry: EntryStaticNormalized) => void;
Expand All @@ -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 = {
Expand Down
Loading