Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import defu from 'defu';
import { Plugin } from 'vite';
import addEntry from './plugins/pluginAddEntry';
import { checkAliasConflicts } from './plugins/pluginCheckAliasConflicts';
import { PluginDevProxyModuleTopLevelAwait } from './plugins/pluginDevProxyModuleTopLevelAwait';
import pluginManifest from './plugins/pluginMFManifest';
import pluginModuleParseEnd from './plugins/pluginModuleParseEnd';
Expand Down Expand Up @@ -41,6 +42,7 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
},
},
aliasToArrayPlugin,
checkAliasConflicts({ shared }),
normalizeOptimizeDepsPlugin,
...addEntry({
entryName: 'remoteEntry',
Expand Down
199 changes: 199 additions & 0 deletions src/plugins/__tests__/pluginCheckAliasConflicts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { checkAliasConflicts } from '../pluginCheckAliasConflicts';

describe('pluginCheckAliasConflicts', () => {
let consoleWarnSpy: any;
let mockLogger: any;

beforeEach(() => {
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
mockLogger = {
warn: vi.fn(),
info: vi.fn(),
error: vi.fn(),
};
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});

it('should warn when alias conflicts with shared module', () => {
const plugin = checkAliasConflicts({
shared: {
vue: {
name: 'vue',
version: '3.2.45',
scope: 'default',
from: 'host',
shareConfig: {
requiredVersion: '^3.2.45',
} as any,
},
pinia: {
name: 'pinia',
version: '2.0.28',
scope: 'default',
from: 'host',
shareConfig: {
requiredVersion: '^2.0.28',
} as any,
},
},
});

const mockConfig = {
logger: mockLogger,
resolve: {
alias: [
{
find: 'vue',
replacement: '/path/to/project/node_modules/vue/dist/vue.runtime.esm-bundler.js',
},
{
find: 'pinia',
replacement: '/path/to/project/node_modules/pinia/dist/pinia.mjs',
},
{
find: 'shared',
replacement: '/path/to/project/shared/shared',
},
],
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).toHaveBeenCalledTimes(5);

Check failure on line 67 in src/plugins/__tests__/pluginCheckAliasConflicts.test.ts

View workflow job for this annotation

GitHub Actions / Unit Test Package

src/plugins/__tests__/pluginCheckAliasConflicts.test.ts > pluginCheckAliasConflicts > should warn when alias conflicts with shared module

AssertionError: expected "spy" to be called 5 times, but got 4 times ❯ src/plugins/__tests__/pluginCheckAliasConflicts.test.ts:67:29
expect(mockLogger.warn).toHaveBeenCalledWith(
'\n[Module Federation] Detected alias conflicts with shared modules:'
);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Shared module "vue" is aliased by "vue"')
);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Shared module "pinia" is aliased by "pinia"')
);
});

it('should not warn when no alias conflicts exist', () => {
const plugin = checkAliasConflicts({
shared: {
vue: {
name: 'vue',
version: '3.2.45',
scope: 'default',
from: 'host',
shareConfig: {
requiredVersion: '^3.2.45',
} as any,
},
},
});

const mockConfig = {
logger: mockLogger,
resolve: {
alias: [
{
find: 'shared',
replacement: '/path/to/project/shared/shared',
},
],
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).not.toHaveBeenCalled();
});

it('should not warn when shared is empty', () => {
const plugin = checkAliasConflicts({
shared: {},
});

const mockConfig = {
logger: mockLogger,
resolve: {
alias: [
{
find: 'vue',
replacement: '/path/to/project/node_modules/vue/dist/vue.runtime.esm-bundler.js',
},
],
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).not.toHaveBeenCalled();
});

it('should handle regex alias patterns', () => {
const plugin = checkAliasConflicts({
shared: {
react: {
name: 'react',
version: '18.0.0',
scope: 'default',
from: 'host',
shareConfig: {
requiredVersion: '^18.0.0',
} as any,
},
},
});

const mockConfig = {
logger: mockLogger,
resolve: {
alias: [
{
find: /^react$/,
replacement: '/path/to/project/node_modules/react/index.js',
},
],
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).toHaveBeenCalled();
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Shared module "react" is aliased')
);
});

it('should handle shared modules with trailing slash', () => {
const plugin = checkAliasConflicts({
shared: {
'lodash/': {
name: 'lodash/',
version: '4.17.21',
scope: 'default',
from: 'host',
shareConfig: {
requiredVersion: '^4.17.21',
} as any,
},
},
});

const mockConfig = {
logger: mockLogger,
resolve: {
alias: [
{
find: 'lodash',
replacement: '/path/to/project/node_modules/lodash',
},
],
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).toHaveBeenCalled();
});
});
59 changes: 59 additions & 0 deletions src/plugins/pluginCheckAliasConflicts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Alias, Plugin } from 'vite';
import { NormalizedShared } from '../utils/normalizeModuleFederationOptions';

/**
* Check if user-defined alias conflicts with shared modules
* This should run after aliasToArrayPlugin to ensure alias is an array
*/
export function checkAliasConflicts(options: { shared?: NormalizedShared }): Plugin {
const { shared = {} } = options;
const sharedKeys = Object.keys(shared);

return {
name: 'check-alias-conflicts',
configResolved(config: any) {
if (sharedKeys.length === 0) return;

const userAliases: Alias[] = config.resolve?.alias || [];
const conflicts: Array<{ sharedModule: string; alias: string; target: string }> = [];

for (const sharedKey of sharedKeys) {
for (const aliasEntry of userAliases) {
const findPattern = aliasEntry.find;
const replacement = aliasEntry.replacement;

// Skip if replacement is not a string (e.g., customResolver)
if (typeof replacement !== 'string') continue;

// Check if alias pattern matches the shared module
let isMatch = false;
if (typeof findPattern === 'string') {
isMatch = findPattern === sharedKey || sharedKey.startsWith(findPattern + '/');
} else if (findPattern instanceof RegExp) {
isMatch = findPattern.test(sharedKey);
}

if (isMatch) {
conflicts.push({
sharedModule: sharedKey,
alias: String(findPattern),
target: replacement,
});
}
}
}

if (conflicts.length > 0) {
config.logger.warn('\n[Module Federation] Detected alias conflicts with shared modules:');
conflicts.forEach(({ sharedModule, alias, target }) => {
config.logger.warn(
` - Shared module "${sharedModule}" is aliased by "${alias}" to "${target}"`
);
});
config.logger.warn(
" This may cause runtime errors as the shared module will bypass Module Federation's sharing mechanism."
);
}
},
};
}
Loading