Skip to content

Commit 9f94369

Browse files
committed
feat: warn if shared dependency is resolved to somewhere (#354)
1 parent 60c7262 commit 9f94369

File tree

3 files changed

+260
-0
lines changed

3 files changed

+260
-0
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import defu from 'defu';
22
import { Plugin } from 'vite';
33
import addEntry from './plugins/pluginAddEntry';
4+
import { checkAliasConflicts } from './plugins/pluginCheckAliasConflicts';
45
import { PluginDevProxyModuleTopLevelAwait } from './plugins/pluginDevProxyModuleTopLevelAwait';
56
import pluginManifest from './plugins/pluginMFManifest';
67
import pluginModuleParseEnd from './plugins/pluginModuleParseEnd';
@@ -41,6 +42,7 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
4142
},
4243
},
4344
aliasToArrayPlugin,
45+
checkAliasConflicts({ shared }),
4446
normalizeOptimizeDepsPlugin,
4547
...addEntry({
4648
entryName: 'remoteEntry',
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { checkAliasConflicts } from '../pluginCheckAliasConflicts';
3+
4+
describe('pluginCheckAliasConflicts', () => {
5+
let consoleWarnSpy: any;
6+
let mockLogger: any;
7+
8+
beforeEach(() => {
9+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
10+
mockLogger = {
11+
warn: vi.fn(),
12+
info: vi.fn(),
13+
error: vi.fn(),
14+
};
15+
});
16+
17+
afterEach(() => {
18+
consoleWarnSpy.mockRestore();
19+
});
20+
21+
it('should warn when alias conflicts with shared module', () => {
22+
const plugin = checkAliasConflicts({
23+
shared: {
24+
vue: {
25+
name: 'vue',
26+
version: '3.2.45',
27+
scope: 'default',
28+
from: 'host',
29+
shareConfig: {
30+
requiredVersion: '^3.2.45',
31+
} as any,
32+
},
33+
pinia: {
34+
name: 'pinia',
35+
version: '2.0.28',
36+
scope: 'default',
37+
from: 'host',
38+
shareConfig: {
39+
requiredVersion: '^2.0.28',
40+
} as any,
41+
},
42+
},
43+
});
44+
45+
const mockConfig = {
46+
logger: mockLogger,
47+
resolve: {
48+
alias: [
49+
{
50+
find: 'vue',
51+
replacement: '/path/to/project/node_modules/vue/dist/vue.runtime.esm-bundler.js',
52+
},
53+
{
54+
find: 'pinia',
55+
replacement: '/path/to/project/node_modules/pinia/dist/pinia.mjs',
56+
},
57+
{
58+
find: 'shared',
59+
replacement: '/path/to/project/shared/shared',
60+
},
61+
],
62+
},
63+
};
64+
65+
plugin.configResolved!(mockConfig as any);
66+
67+
expect(mockLogger.warn).toHaveBeenCalledTimes(5);
68+
expect(mockLogger.warn).toHaveBeenCalledWith(
69+
'\n[Module Federation] Detected alias conflicts with shared modules:'
70+
);
71+
expect(mockLogger.warn).toHaveBeenCalledWith(
72+
expect.stringContaining('Shared module "vue" is aliased by "vue"')
73+
);
74+
expect(mockLogger.warn).toHaveBeenCalledWith(
75+
expect.stringContaining('Shared module "pinia" is aliased by "pinia"')
76+
);
77+
});
78+
79+
it('should not warn when no alias conflicts exist', () => {
80+
const plugin = checkAliasConflicts({
81+
shared: {
82+
vue: {
83+
name: 'vue',
84+
version: '3.2.45',
85+
scope: 'default',
86+
from: 'host',
87+
shareConfig: {
88+
requiredVersion: '^3.2.45',
89+
} as any,
90+
},
91+
},
92+
});
93+
94+
const mockConfig = {
95+
logger: mockLogger,
96+
resolve: {
97+
alias: [
98+
{
99+
find: 'shared',
100+
replacement: '/path/to/project/shared/shared',
101+
},
102+
],
103+
},
104+
};
105+
106+
plugin.configResolved!(mockConfig as any);
107+
108+
expect(mockLogger.warn).not.toHaveBeenCalled();
109+
});
110+
111+
it('should not warn when shared is empty', () => {
112+
const plugin = checkAliasConflicts({
113+
shared: {},
114+
});
115+
116+
const mockConfig = {
117+
logger: mockLogger,
118+
resolve: {
119+
alias: [
120+
{
121+
find: 'vue',
122+
replacement: '/path/to/project/node_modules/vue/dist/vue.runtime.esm-bundler.js',
123+
},
124+
],
125+
},
126+
};
127+
128+
plugin.configResolved!(mockConfig as any);
129+
130+
expect(mockLogger.warn).not.toHaveBeenCalled();
131+
});
132+
133+
it('should handle regex alias patterns', () => {
134+
const plugin = checkAliasConflicts({
135+
shared: {
136+
react: {
137+
name: 'react',
138+
version: '18.0.0',
139+
scope: 'default',
140+
from: 'host',
141+
shareConfig: {
142+
requiredVersion: '^18.0.0',
143+
} as any,
144+
},
145+
},
146+
});
147+
148+
const mockConfig = {
149+
logger: mockLogger,
150+
resolve: {
151+
alias: [
152+
{
153+
find: /^react$/,
154+
replacement: '/path/to/project/node_modules/react/index.js',
155+
},
156+
],
157+
},
158+
};
159+
160+
plugin.configResolved!(mockConfig as any);
161+
162+
expect(mockLogger.warn).toHaveBeenCalled();
163+
expect(mockLogger.warn).toHaveBeenCalledWith(
164+
expect.stringContaining('Shared module "react" is aliased')
165+
);
166+
});
167+
168+
it('should handle shared modules with trailing slash', () => {
169+
const plugin = checkAliasConflicts({
170+
shared: {
171+
'lodash/': {
172+
name: 'lodash/',
173+
version: '4.17.21',
174+
scope: 'default',
175+
from: 'host',
176+
shareConfig: {
177+
requiredVersion: '^4.17.21',
178+
} as any,
179+
},
180+
},
181+
});
182+
183+
const mockConfig = {
184+
logger: mockLogger,
185+
resolve: {
186+
alias: [
187+
{
188+
find: 'lodash',
189+
replacement: '/path/to/project/node_modules/lodash',
190+
},
191+
],
192+
},
193+
};
194+
195+
plugin.configResolved!(mockConfig as any);
196+
197+
expect(mockLogger.warn).toHaveBeenCalled();
198+
});
199+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Alias, Plugin } from 'vite';
2+
import { NormalizedShared } from '../utils/normalizeModuleFederationOptions';
3+
4+
/**
5+
* Check if user-defined alias conflicts with shared modules
6+
* This should run after aliasToArrayPlugin to ensure alias is an array
7+
*/
8+
export function checkAliasConflicts(options: { shared?: NormalizedShared }): Plugin {
9+
const { shared = {} } = options;
10+
const sharedKeys = Object.keys(shared);
11+
12+
return {
13+
name: 'check-alias-conflicts',
14+
configResolved(config: any) {
15+
if (sharedKeys.length === 0) return;
16+
17+
const userAliases: Alias[] = config.resolve?.alias || [];
18+
const conflicts: Array<{ sharedModule: string; alias: string; target: string }> = [];
19+
20+
for (const sharedKey of sharedKeys) {
21+
for (const aliasEntry of userAliases) {
22+
const findPattern = aliasEntry.find;
23+
const replacement = aliasEntry.replacement;
24+
25+
// Skip if replacement is not a string (e.g., customResolver)
26+
if (typeof replacement !== 'string') continue;
27+
28+
// Check if alias pattern matches the shared module
29+
let isMatch = false;
30+
if (typeof findPattern === 'string') {
31+
isMatch = findPattern === sharedKey || sharedKey.startsWith(findPattern + '/');
32+
} else if (findPattern instanceof RegExp) {
33+
isMatch = findPattern.test(sharedKey);
34+
}
35+
36+
if (isMatch) {
37+
conflicts.push({
38+
sharedModule: sharedKey,
39+
alias: String(findPattern),
40+
target: replacement,
41+
});
42+
}
43+
}
44+
}
45+
46+
if (conflicts.length > 0) {
47+
config.logger.warn('\n[Module Federation] Detected alias conflicts with shared modules:');
48+
conflicts.forEach(({ sharedModule, alias, target }) => {
49+
config.logger.warn(
50+
` - Shared module "${sharedModule}" is aliased by "${alias}" to "${target}"`
51+
);
52+
});
53+
config.logger.warn(
54+
" This may cause runtime errors as the shared module will bypass Module Federation's sharing mechanism."
55+
);
56+
}
57+
},
58+
};
59+
}

0 commit comments

Comments
 (0)