Skip to content

Commit 48abe94

Browse files
authored
feat: add publicPath option (#332)
1 parent 1e466bf commit 48abe94

File tree

6 files changed

+109
-4
lines changed

6 files changed

+109
-4
lines changed

src/plugins/pluginMFManifest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
processModuleAssets,
1818
trackAsset,
1919
} from '../utils/cssModuleHelpers';
20+
import { resolvePublicPath } from '../utils/publicPath';
2021

2122
// Helper to build share key map with proper context typing
2223
interface BuildFileToShareKeyMapContext {
@@ -138,8 +139,7 @@ const Manifest = (): Plugin[] => {
138139
if (_command === 'serve') {
139140
base = (config.server.origin || '') + config.base;
140141
}
141-
publicPath =
142-
_originalConfigBase === '' ? 'auto' : base ? base.replace(/\/?$/, '/') : 'auto';
142+
publicPath = resolvePublicPath(mfOptions, base, _originalConfigBase);
143143
},
144144
/**
145145
* Generates the module federation manifest file

src/plugins/pluginProxyRemoteEntry.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
VIRTUAL_EXPOSES,
1010
} from '../virtualModules';
1111
import { parsePromise } from './pluginModuleParseEnd';
12+
import { resolvePublicPath } from '../utils/publicPath';
1213

1314
const filter: (id: string) => boolean = createFilter();
1415

@@ -60,10 +61,13 @@ export default function (): Plugin {
6061
typeof viteConfig.server?.host === 'string' && viteConfig.server.host !== '0.0.0.0'
6162
? viteConfig.server.host
6263
: 'localhost';
64+
const publicPath = JSON.stringify(
65+
resolvePublicPath(options, viteConfig.base) + options.filename
66+
);
6367
return `
6468
const origin = (window && ${!options.ignoreOrigin}) ? window.origin : "//${host}:${viteConfig.server?.port}"
65-
const remoteEntryPromise = await import(origin + "${viteConfig.base + options.filename}")
66-
// __tla only serves as a hack for vite-plugin-top-level-await.
69+
const remoteEntryPromise = await import(origin + ${publicPath})
70+
// __tla only serves as a hack for vite-plugin-top-level-await.
6771
Promise.resolve(remoteEntryPromise)
6872
.then(remoteEntry => {
6973
return Promise.resolve(remoteEntry.__tla)

src/utils/__tests__/helpers.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { NormalizedModuleFederationOptions } from '../normalizeModuleFederationOptions';
2+
3+
export function getDefaultMockOptions(
4+
overrides: Partial<NormalizedModuleFederationOptions> = {}
5+
): NormalizedModuleFederationOptions {
6+
return {
7+
exposes: {},
8+
filename: 'remoteEntry.js',
9+
library: {},
10+
name: 'test',
11+
remotes: {},
12+
runtime: {},
13+
shareScope: 'default',
14+
shared: {},
15+
runtimePlugins: [],
16+
implementation: require.resolve('@module-federation/runtime'),
17+
manifest: false,
18+
shareStrategy: 'loaded-first',
19+
virtualModuleDir: '__mf__virtual',
20+
...overrides,
21+
};
22+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { resolvePublicPath } from '../publicPath';
3+
import { getDefaultMockOptions } from './helpers';
4+
5+
describe('resolvePublicPath', () => {
6+
const mockOptions = getDefaultMockOptions();
7+
8+
it('should return explicitly set publicPath when provided', () => {
9+
const options = {
10+
...mockOptions,
11+
publicPath: 'https://cdn.example.com/',
12+
};
13+
expect(resolvePublicPath(options, '/vite/')).toBe('https://cdn.example.com/');
14+
});
15+
16+
it('should return "auto" when originalBase is empty string', () => {
17+
expect(resolvePublicPath(mockOptions, '/vite/', '')).toBe('auto');
18+
});
19+
20+
it('should return viteBase with trailing slash when provided', () => {
21+
expect(resolvePublicPath(mockOptions, '/vite')).toBe('/vite/');
22+
expect(resolvePublicPath(mockOptions, '/vite/')).toBe('/vite/');
23+
});
24+
25+
it('should return "auto" when no base is specified', () => {
26+
expect(resolvePublicPath(mockOptions, '')).toBe('auto');
27+
});
28+
29+
it('should return "auto" when base is undefined', () => {
30+
expect(resolvePublicPath(mockOptions, undefined as unknown as string)).toBe('auto');
31+
});
32+
33+
it('should prioritize publicPath over viteBase when both are provided', () => {
34+
const options = {
35+
...mockOptions,
36+
publicPath: 'https://custom.example/',
37+
};
38+
expect(resolvePublicPath(options, '/vite/')).toBe('https://custom.example/');
39+
});
40+
});

src/utils/normalizeModuleFederationOptions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ export type ModuleFederationOptions = {
277277
remotes?: Record<string, string | RemoteObjectConfig> | undefined;
278278
runtime?: any;
279279
shareScope?: string;
280+
/**
281+
* Override the public path used for remote entries
282+
* Defaults to Vite's base config or "auto" if base is empty
283+
*/
284+
publicPath?: string;
280285
shared?:
281286
| string[]
282287
| Record<
@@ -320,6 +325,7 @@ export interface NormalizedModuleFederationOptions {
320325
dts?: boolean | PluginDtsOptions;
321326
shareStrategy: ShareStrategy;
322327
getPublicPath?: string;
328+
publicPath?: string;
323329
ignoreOrigin?: boolean;
324330
virtualModuleDir: string;
325331
}
@@ -406,6 +412,7 @@ export function normalizeModuleFederationOptions(
406412
dev: options.dev,
407413
dts: options.dts,
408414
getPublicPath: options.getPublicPath,
415+
publicPath: options.publicPath,
409416
shareStrategy: options.shareStrategy || 'version-first',
410417
ignoreOrigin: options.ignoreOrigin || false,
411418
virtualModuleDir: options.virtualModuleDir || '__mf__virtual',

src/utils/publicPath.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NormalizedModuleFederationOptions } from './normalizeModuleFederationOptions';
2+
3+
/**
4+
* Resolves the public path for remote entries
5+
* @param options - Module Federation options
6+
* @param viteBase - Vite's base config value
7+
* @param originalBase - Original base config before any transformations
8+
* @returns The resolved public path
9+
*/
10+
export function resolvePublicPath(
11+
options: NormalizedModuleFederationOptions,
12+
viteBase: string,
13+
originalBase?: string
14+
): string {
15+
// Use explicitly set publicPath if provided
16+
if (options.publicPath) {
17+
return options.publicPath;
18+
}
19+
20+
// Handle empty original base case
21+
if (originalBase === '') {
22+
return 'auto';
23+
}
24+
25+
// Use viteBase if available, ensuring it ends with a slash
26+
if (viteBase) {
27+
return viteBase.replace(/\/?$/, '/');
28+
}
29+
30+
// Fallback to auto if no base is specified
31+
return 'auto';
32+
}

0 commit comments

Comments
 (0)