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
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
226 changes: 226 additions & 0 deletions src/plugins/__tests__/pluginCheckAliasConflicts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
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(4);
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();
});

it('should work with undefined alias', () => {
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: undefined,
},
};

plugin.configResolved!(mockConfig as any);

expect(mockLogger.warn).not.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."
);
}
},
};
}