Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3d6d303
fix(enhanced): default remoteType to script
cursoragent Feb 9, 2026
7a6822e
test(enhanced): cover remoteType default
cursoragent Feb 9, 2026
44d291b
chore: add changeset for remoteType default
cursoragent Feb 9, 2026
73db73d
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 9, 2026
403ec3d
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 9, 2026
2e769a9
fix: format ModuleFederationPlugin test to pass nx format:check
ScriptedAlchemy Feb 9, 2026
8983213
fix(enhanced): lazy-require RemoteEntryPlugin and restore remoteType …
ScriptedAlchemy Feb 9, 2026
8f98c88
Merge remote-tracking branch 'origin/main' into cursor/module-federat…
ScriptedAlchemy Feb 9, 2026
a805d70
fix(enhanced): keep remoteType defaults but force script for manifest
ScriptedAlchemy Feb 9, 2026
1911c9e
Merge remote-tracking branch 'origin/main' into cursor/module-federat…
ScriptedAlchemy Feb 12, 2026
77452d6
chore(core): add changeset coverage for pr #4397
ScriptedAlchemy Feb 12, 2026
d3e1565
Merge branch 'cursor/module-federation-issue-4294-9fd0' of github.com…
ScriptedAlchemy Feb 12, 2026
4c48237
chore(enhanced): remove redundant auto changeset
ScriptedAlchemy Feb 12, 2026
f2910c9
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 14, 2026
8f67122
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 14, 2026
46e70cf
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 15, 2026
b1051d3
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 15, 2026
ea9a7f5
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 16, 2026
4714731
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 19, 2026
29a3c71
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 19, 2026
7416fa7
Merge remote-tracking branch 'origin/main' into cursor/module-federat…
ScriptedAlchemy Feb 24, 2026
9c02be1
fix(dts-plugin): align workspace entrypoints and RawSource typing
ScriptedAlchemy Feb 24, 2026
71976f3
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 25, 2026
3b419b6
fix(sdk): align package entrypoints with emitted artifacts
ScriptedAlchemy Feb 25, 2026
b5480ae
fix(enhanced): restore typed remoteType for container references
ScriptedAlchemy Feb 25, 2026
beb851e
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 25, 2026
6346629
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 26, 2026
b1c6113
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 26, 2026
5d67ba2
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 27, 2026
b590129
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 27, 2026
f6655aa
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 28, 2026
1a4f57c
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Feb 28, 2026
44111f9
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Mar 2, 2026
05bb08a
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Mar 3, 2026
7d17e78
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Mar 5, 2026
fe7a0be
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Mar 9, 2026
b757526
Merge branch 'main' into cursor/module-federation-issue-4294-9fd0
ScriptedAlchemy Mar 10, 2026
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/short-crabs-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@module-federation/enhanced": patch
---

Default `remoteType` to `script` so UMD library builds generate remote references correctly.
62 changes: 53 additions & 9 deletions packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import SharePlugin from '../sharing/SharePlugin';
import ContainerPlugin from './ContainerPlugin';
import ContainerReferencePlugin from './ContainerReferencePlugin';
import FederationRuntimePlugin from './runtime/FederationRuntimePlugin';
import { RemoteEntryPlugin } from '@module-federation/rspack/remote-entry-plugin';
import StartupChunkDependenciesPlugin from '../startup/MfStartupChunkDependenciesPlugin';
import FederationModulesPlugin from './runtime/FederationModulesPlugin';
import { createSchemaValidation } from '../../utils';
Expand Down Expand Up @@ -144,6 +143,9 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
throw new Error('ModuleFederationPlugin name is required');
}
// must before ModuleFederationPlugin
// Use runtime require here to avoid import cycles (and to play nicely with test-time mocking).
const { RemoteEntryPlugin } =
require('@module-federation/rspack/remote-entry-plugin') as typeof import('@module-federation/rspack/remote-entry-plugin');
(new RemoteEntryPlugin(options) as unknown as WebpackPluginInstance).apply(
compiler,
);
Expand Down Expand Up @@ -194,13 +196,55 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
new FederationRuntimePlugin(options).apply(compiler);

const library = options.library || { type: 'var', name: name };
const remoteType =
options.remoteType ||
(options.library && isValidExternalsType(options.library.type)
? (options.library.type as moduleFederationPlugin.ExternalsType)
: ('script' as moduleFederationPlugin.ExternalsType));
const containerRemoteType =
remoteType as moduleFederationPlugin.ExternalsType;
// Default remoteType to 'script' unless the library type itself is a
// well-known externals type that works as a remoteType (e.g. 'var',
// 'module'). Notably, 'umd' is excluded because it isn't a valid
// remoteType for ContainerReferencePlugin.
const validRemoteTypes = new Set([
'var',
'module',
'assign',
'this',
'window',
'self',
'global',
'commonjs',
'commonjs2',
'commonjs-module',
'commonjs-static',
'amd',
'amd-require',
'system',
'jsonp',
'import',
'script',
'node-commonjs',
'promise',
]);

const isManifestRemote = (value: unknown): boolean => {
// Manifest remote syntax is typically: "name@http(s)://.../mf-manifest.json"
if (typeof value !== 'string') return false;
return /mf-manifest\.json(\?|#|$)/.test(value);
};

const remotesContainManifest = (() => {
if (!remotes) return false;
if (Array.isArray(remotes)) return remotes.some(isManifestRemote);
// Object form: { [key]: string | ... }. Only handle string values here.
return Object.values(remotes as Record<string, unknown>).some(
isManifestRemote,
);
})();

const remoteType = (options.remoteType ??
(remotesContainManifest
? 'script'
: options.library && validRemoteTypes.has(options.library.type)
? options.library.type
: 'script')) as NonNullable<
moduleFederationPlugin.ModuleFederationPluginOptions['remoteType']
>;

let disableManifest = options.manifest === false;
if (useContainerPlugin) {
Expand Down Expand Up @@ -247,7 +291,7 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
: Object.keys(remotes).length > 0)
) {
new ContainerReferencePlugin({
remoteType: containerRemoteType,
remoteType,
shareScope,
remotes,
}).apply(compiler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ export default class IndependentSharedPlugin {
const shareRequestsMap: ShareRequestsMap =
await this.createIndependentCompiler(parentCompiler);

const normalizeRequest = (resource: string) => {
// The collector records resolved absolute resources. For independent fallback bundles we want
// webpack to resolve the module from the same context as the parent compilation, otherwise
// some setups end up with a runtime "Cannot find module ..." stub instead of bundling.
if (typeof resource !== 'string' || !resource) return resource;
if (!path.isAbsolute(resource)) return resource;
const rel = path.relative(parentCompiler.context, resource);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return resource;
return `./${rel.split(path.sep).join('/')}`;
};

await Promise.all(
sharedOptions.map(async ([shareName, shareConfig]) => {
if (!shareConfig.treeShaking) {
Expand All @@ -258,7 +269,7 @@ export default class IndependentSharedPlugin {
currentShare: {
shareName,
version,
request,
request: normalizeRequest(request),
independentShareFileName: sharedConfig?.treeShaking?.filename,
},
});
Expand Down
252 changes: 252 additions & 0 deletions packages/enhanced/test/unit/container/ModuleFederationPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { rs, type Mock } from '@rstest/core';
import { createMockCompiler, createWebpackMock } from './utils';

const webpack = {
...createWebpackMock(),
DefinePlugin: class DefinePlugin {
apply = rs.fn();
constructor(_options: Record<string, string | boolean>) {}
},
};

const mocks = rs.hoisted(() => {
const captured = {
containerReferenceOptions: null as null | { remoteType?: string },
};

const mockContainerReferenceApply = rs.fn();

return {
captured,
mockBindLoggerToCompiler: rs.fn(),
mockComposeKeyWithSeparator: rs.fn(
(name: string, version: string) => `${name}-${version}`,
),
mockInfrastructureLogger: {
warn: rs.fn(),
info: rs.fn(),
log: rs.fn(),
},
mockGetBuildVersion: rs.fn(() => 'build'),
mockContainerManager: rs.fn().mockImplementation(() => ({
init: rs.fn(),
containerPluginExposesOptions: {},
})),
mockRemoteEntryPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockFederationRuntimePlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockFederationModulesPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockContainerPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockContainerReferenceApply,
mockContainerReferencePlugin: rs
.fn()
.mockImplementation((options: { remoteType?: string }) => {
captured.containerReferenceOptions = options;
return {
apply: mockContainerReferenceApply,
};
}),
mockSharePlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockTreeShakingSharedPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockStartupChunkDependenciesPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockStatsPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
mockDtsPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
addRuntimePlugins: rs.fn(),
})),
mockPrefetchPlugin: rs.fn().mockImplementation(() => ({
apply: rs.fn(),
})),
};
});

rs.mock('webpack', () => webpack);

rs.mock('@module-federation/sdk/normalize-webpack-path', () => ({
normalizeWebpackPath: (path: string) => path,
}));

rs.mock('@module-federation/sdk', () => ({
bindLoggerToCompiler: mocks.mockBindLoggerToCompiler,
composeKeyWithSeparator: mocks.mockComposeKeyWithSeparator,
infrastructureLogger: mocks.mockInfrastructureLogger,
}));

rs.mock('@module-federation/managers', () => ({
ContainerManager: mocks.mockContainerManager,
utils: {
getBuildVersion: mocks.mockGetBuildVersion,
},
}));

rs.mock('@module-federation/manifest', () => ({
StatsPlugin: mocks.mockStatsPlugin,
}));

rs.mock('@module-federation/dts-plugin', () => ({
DtsPlugin: mocks.mockDtsPlugin,
}));

rs.mock('@module-federation/data-prefetch/cli', () => ({
PrefetchPlugin: mocks.mockPrefetchPlugin,
}));

rs.mock('@module-federation/rspack/remote-entry-plugin', () => ({
RemoteEntryPlugin: mocks.mockRemoteEntryPlugin,
}));

rs.mock('../../../src/utils', () => ({
createSchemaValidation: () => () => undefined,
}));

rs.mock('../../../src/lib/container/runtime/FederationRuntimePlugin', () => ({
__esModule: true,
default: mocks.mockFederationRuntimePlugin,
}));

rs.mock('../../../src/lib/container/runtime/FederationModulesPlugin', () => ({
__esModule: true,
default: mocks.mockFederationModulesPlugin,
}));

rs.mock('../../../src/lib/container/ContainerPlugin', () => ({
__esModule: true,
default: mocks.mockContainerPlugin,
}));

rs.mock('../../../src/lib/container/ContainerReferencePlugin', () => ({
__esModule: true,
default: mocks.mockContainerReferencePlugin,
}));

rs.mock('../../../src/lib/sharing/SharePlugin', () => ({
__esModule: true,
default: mocks.mockSharePlugin,
}));

rs.mock(
'../../../src/lib/sharing/tree-shaking/TreeShakingSharedPlugin',
() => ({
__esModule: true,
default: mocks.mockTreeShakingSharedPlugin,
}),
);

rs.mock('../../../src/lib/startup/MfStartupChunkDependenciesPlugin', () => ({
__esModule: true,
default: mocks.mockStartupChunkDependenciesPlugin,
}));

const ModuleFederationPlugin =
require('../../../src/lib/container/ModuleFederationPlugin').default;

const getTap = <Args extends unknown[]>(
tapMock: Mock,
name: string,
): ((...args: Args) => unknown) | undefined => {
const entry = tapMock.mock.calls.find((call: unknown[]) => call[0] === name);
return entry ? (entry[1] as (...args: Args) => unknown) : undefined;
};

describe('ModuleFederationPlugin remoteType defaults', () => {
let mockCompiler: ReturnType<typeof createMockCompiler>;

beforeEach(() => {
rs.clearAllMocks();
mocks.captured.containerReferenceOptions = null;
mockCompiler = createMockCompiler();
mockCompiler.hooks.afterPlugins = { tap: rs.fn() };
mockCompiler.options.output = {
...mockCompiler.options.output,
enabledLibraryTypes: [],
};
mockCompiler.options.plugins = [];
mockCompiler.webpack = {
...mockCompiler.webpack,
DefinePlugin: webpack.DefinePlugin,
} as any;
});

it('defaults remoteType to script for manifest remotes (umd library)', () => {
const plugin = new ModuleFederationPlugin({
name: 'host',
library: { type: 'umd', name: 'host' },
remotes: {
hostapp: 'hostapp@http://localhost:3000/mf-manifest.json',
},
manifest: false,
dts: false,
});

plugin.apply(mockCompiler as any);

const afterPluginsTap = getTap(
mockCompiler.hooks.afterPlugins.tap as unknown as Mock,
'ModuleFederationPlugin',
);
afterPluginsTap?.();

expect(mocks.captured.containerReferenceOptions?.remoteType).toBe('script');
});

it('defaults remoteType to library type for non-manifest remotes', () => {
const plugin = new ModuleFederationPlugin({
name: 'host',
library: { type: 'var', name: 'host' },
remotes: {
// Non-manifest remote: should not force "script"
remoteA: 'remoteA@http://localhost:3001/remoteEntry.js',
},
manifest: false,
dts: false,
});

plugin.apply(mockCompiler as any);

const afterPluginsTap = getTap(
mockCompiler.hooks.afterPlugins.tap as unknown as Mock,
'ModuleFederationPlugin',
);
afterPluginsTap?.();

expect(mocks.captured.containerReferenceOptions?.remoteType).toBe('var');
});

it('respects explicit remoteType', () => {
const plugin = new ModuleFederationPlugin({
name: 'host',
library: { type: 'umd', name: 'host' },
remotes: {
hostapp: 'hostapp@http://localhost:3000/mf-manifest.json',
},
remoteType: 'umd',
manifest: false,
dts: false,
});

plugin.apply(mockCompiler as any);

const afterPluginsTap = getTap(
mockCompiler.hooks.afterPlugins.tap as unknown as Mock,
'ModuleFederationPlugin',
);
afterPluginsTap?.();

expect(mocks.captured.containerReferenceOptions?.remoteType).toBe('umd');
});
});
Loading