Skip to content

Commit 0eee65f

Browse files
committed
feat: add support for varFilename (generates additional globalVar remoteEntry.js)
1 parent b65d576 commit 0eee65f

File tree

5 files changed

+176
-11
lines changed

5 files changed

+176
-11
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import pluginModuleParseEnd from './plugins/pluginModuleParseEnd';
77
import pluginProxyRemoteEntry from './plugins/pluginProxyRemoteEntry';
88
import pluginProxyRemotes from './plugins/pluginProxyRemotes';
99
import { proxySharedModule } from './plugins/pluginProxySharedModule_preBuild';
10+
import pluginVarRemoteEntry from './plugins/pluginVarRemoteEntry';
1011
import aliasToArrayPlugin from './utils/aliasToArrayPlugin';
1112
import {
1213
ModuleFederationOptions,
@@ -95,6 +96,7 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
9596
},
9697
},
9798
...pluginManifest(),
99+
...pluginVarRemoteEntry(),
98100
];
99101
}
100102

src/plugins/pluginMFManifest.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as path from 'pathe';
2-
import { Plugin } from 'vite';
32
import type { PluginContext } from 'rollup';
3+
import { Plugin } from 'vite';
44
import {
55
getNormalizeModuleFederationOptions,
66
getNormalizeShareItem,
77
} from '../utils/normalizeModuleFederationOptions';
88
import { getUsedRemotesMap, getUsedShares } from '../virtualModules';
99

10+
import { findRemoteEntryFile } from '../utils/bundleHelpers';
1011
import {
1112
buildFileToShareKeyMap,
1213
collectCssAssets,
@@ -26,7 +27,7 @@ interface BuildFileToShareKeyMapContext {
2627

2728
const Manifest = (): Plugin[] => {
2829
const mfOptions = getNormalizeModuleFederationOptions();
29-
const { name, filename, getPublicPath, manifest: manifestOptions } = mfOptions;
30+
const { name, filename, getPublicPath, manifest: manifestOptions, varFilename } = mfOptions;
3031

3132
let mfManifestName: string = '';
3233
if (manifestOptions === true) {
@@ -104,6 +105,13 @@ const Manifest = (): Plugin[] => {
104105
path: '',
105106
type: 'module',
106107
},
108+
varRemoteEntry: varFilename
109+
? {
110+
name: varFilename,
111+
path: '',
112+
type: 'var',
113+
}
114+
: undefined,
107115
types: { path: '', name: '' },
108116
globalName: name,
109117
pluginVersion: '0.2.5',
@@ -151,15 +159,11 @@ const Manifest = (): Plugin[] => {
151159

152160
let filesMap: PreloadMap = {};
153161

162+
const foundRemoteEntryFile = findRemoteEntryFile(mfOptions.filename, bundle);
163+
154164
// First pass: Find remoteEntry file
155-
for (const [_, fileData] of Object.entries(bundle)) {
156-
if (
157-
mfOptions.filename.replace(/[\[\]]/g, '_').replace(/\.[^/.]+$/, '') === fileData.name ||
158-
fileData.name === 'remoteEntry'
159-
) {
160-
remoteEntryFile = fileData.fileName;
161-
break; // We can break early since we only need to find remoteEntry once
162-
}
165+
if (foundRemoteEntryFile) {
166+
remoteEntryFile = foundRemoteEntryFile;
163167
}
164168

165169
// Second pass: Collect all CSS assets
@@ -224,13 +228,21 @@ const Manifest = (): Plugin[] => {
224228
*/
225229
function generateMFManifest(preloadMap: PreloadMap) {
226230
const options = getNormalizeModuleFederationOptions();
227-
const { name } = options;
231+
const { name, varFilename } = options;
228232
const remoteEntry = {
229233
name: remoteEntryFile,
230234
path: '',
231235
type: 'module',
232236
};
233237

238+
const varRemoteEntry = varFilename
239+
? {
240+
name: varFilename,
241+
path: '',
242+
type: 'module',
243+
}
244+
: undefined;
245+
234246
// Process remotes
235247
const remotes = Array.from(Object.entries(getUsedRemotesMap())).flatMap(
236248
([remoteKey, modules]) =>
@@ -304,6 +316,7 @@ const Manifest = (): Plugin[] => {
304316
},
305317
remoteEntry,
306318
ssrRemoteEntry: remoteEntry,
319+
varRemoteEntry,
307320
types: {
308321
path: '',
309322
name: '',
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Plugin } from 'vite';
2+
import { findRemoteEntryFile } from '../utils/bundleHelpers';
3+
import { warn } from '../utils/logUtils';
4+
import { getNormalizeModuleFederationOptions } from '../utils/normalizeModuleFederationOptions';
5+
6+
const VarRemoteEntry = (): Plugin[] => {
7+
const mfOptions = getNormalizeModuleFederationOptions();
8+
const { name, varFilename, filename } = mfOptions;
9+
10+
let viteConfig: any;
11+
12+
return [
13+
{
14+
name: 'module-federation-var-remote-entry',
15+
apply: 'serve',
16+
/**
17+
* Stores resolved Vite config for later use
18+
*/
19+
/**
20+
* Finalizes configuration after all plugins are resolved
21+
* @param config - Fully resolved Vite config
22+
*/
23+
configResolved(config) {
24+
viteConfig = config;
25+
},
26+
/**
27+
* Configures dev server middleware to handle varRemoteEntry requests
28+
* @param server - Vite dev server instance
29+
*/
30+
configureServer(server) {
31+
server.middlewares.use((req, res, next) => {
32+
if (!varFilename) {
33+
next();
34+
return;
35+
}
36+
if (
37+
req.url?.replace(/\?.*/, '') === (viteConfig.base + varFilename).replace(/^\/?/, '/')
38+
) {
39+
res.setHeader('Content-Type', 'text/javascript');
40+
res.setHeader('Access-Control-Allow-Origin', '*');
41+
console.log({ filename });
42+
res.end(generateVarRemoteEntry(filename));
43+
} else {
44+
next();
45+
}
46+
});
47+
},
48+
},
49+
{
50+
name: 'module-federation-var-remote-entry',
51+
enforce: 'post',
52+
/**
53+
* Initial plugin configuration
54+
* @param config - Vite config object
55+
* @param command - Current Vite command (serve/build)
56+
*/
57+
config(config, { command }) {
58+
if (!config.build) config.build = {};
59+
},
60+
/**
61+
* Generates the module federation "var" remote entry file
62+
* @param options - Rollup output options
63+
* @param bundle - Generated bundle assets
64+
*/
65+
async generateBundle(options, bundle) {
66+
if (!varFilename) return;
67+
68+
const isValidName = isValidVarName(name);
69+
70+
if (!isValidName) {
71+
warn(
72+
`Provided remote name "${name}" is not valid for "var" remoteEntry type, thus it's placed in globalThis['${name}'].\nIt may cause problems, so you would better want to use valid var name (see https://www.w3schools.com/js/js_variables.asp).`
73+
);
74+
}
75+
76+
const remoteEntryFile = findRemoteEntryFile(mfOptions.filename, bundle);
77+
78+
if (!remoteEntryFile)
79+
throw new Error(
80+
`Couldn't find a remoteEntry chunk file for ${mfOptions.filename}, can't generate varRemoteEntry file`
81+
);
82+
83+
this.emitFile({
84+
type: 'asset',
85+
fileName: varFilename,
86+
source: generateVarRemoteEntry(remoteEntryFile),
87+
});
88+
},
89+
},
90+
];
91+
92+
function isValidVarName(name: string) {
93+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
94+
}
95+
96+
/**
97+
* Generates the final "var" remote entry file
98+
* @param remoteEntryFile - Path to esm remote entry file
99+
* @returns Complete "var" remoteEntry.js file source
100+
*/
101+
function generateVarRemoteEntry(remoteEntryFile: string) {
102+
const options = getNormalizeModuleFederationOptions();
103+
104+
const { name, varFilename } = options;
105+
106+
const isValidName = isValidVarName(name);
107+
108+
// @TODO: implement publicPath/getPublicPath support
109+
return `
110+
${isValidName ? `var ${name};` : ''}
111+
${isValidName ? name : `globalThis['${name}']`} = (function () {
112+
function getScriptUrl() {
113+
const currentScript = document.currentScript;
114+
if (!currentScript) {
115+
console.error("[VarRemoteEntry] ${varFilename} script should be called from sync <script> tag (document.currentScript is undefined)")
116+
return '/';
117+
}
118+
return document.currentScript.src.replace(/\\/[^/]*$/, '/');
119+
}
120+
121+
const entry = getScriptUrl() + '${remoteEntryFile}';
122+
123+
return {
124+
get: (...args) => import(entry).then(m => m.get(...args)),
125+
init: (...args) => import(entry).then(m => m.init(...args)),
126+
};
127+
})();
128+
`;
129+
}
130+
};
131+
132+
export default VarRemoteEntry;

src/utils/bundleHelpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { OutputBundle } from 'rollup';
2+
3+
export function findRemoteEntryFile(filename: string, bundle: OutputBundle) {
4+
for (const [_, fileData] of Object.entries(bundle)) {
5+
if (
6+
filename.replace(/[\[\]]/g, '_').replace(/\.[^/.]+$/, '') === fileData.name ||
7+
fileData.name === 'remoteEntry'
8+
) {
9+
return fileData.fileName; // We can return early since we only need to find remoteEntry once
10+
}
11+
}
12+
}

src/utils/normalizeModuleFederationOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ export type ModuleFederationOptions = {
318318
ignoreOrigin?: boolean;
319319
virtualModuleDir?: string;
320320
hostInitInjectLocation?: HostInitInjectLocationOptions;
321+
/**
322+
* Allows generate additional remoteEntry file for "var" host environment
323+
*/
324+
varFilename?: string;
321325
};
322326

323327
export interface NormalizedModuleFederationOptions {
@@ -347,6 +351,7 @@ export interface NormalizedModuleFederationOptions {
347351
* When true, all CSS assets are bundled into every exposed module.
348352
*/
349353
bundleAllCSS: boolean;
354+
varFilename?: string;
350355
}
351356

352357
type HostInitInjectLocationOptions = 'entry' | 'html';
@@ -439,5 +444,6 @@ export function normalizeModuleFederationOptions(
439444
virtualModuleDir: options.virtualModuleDir || '__mf__virtual',
440445
hostInitInjectLocation: options.hostInitInjectLocation || 'html',
441446
bundleAllCSS: options.bundleAllCSS || false,
447+
varFilename: options.varFilename,
442448
});
443449
}

0 commit comments

Comments
 (0)