From 0eee65f1a94a83de5defc68e5f33cd57b2072d9a Mon Sep 17 00:00:00 2001 From: Konstantin Barabanov Date: Mon, 10 Nov 2025 16:53:15 +0300 Subject: [PATCH 1/2] feat: add support for varFilename (generates additional globalVar remoteEntry.js) --- src/index.ts | 2 + src/plugins/pluginMFManifest.ts | 35 +++-- src/plugins/pluginVarRemoteEntry.ts | 132 ++++++++++++++++++ src/utils/bundleHelpers.ts | 12 ++ src/utils/normalizeModuleFederationOptions.ts | 6 + 5 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/plugins/pluginVarRemoteEntry.ts create mode 100644 src/utils/bundleHelpers.ts diff --git a/src/index.ts b/src/index.ts index 86ad878..20f99a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import pluginModuleParseEnd from './plugins/pluginModuleParseEnd'; import pluginProxyRemoteEntry from './plugins/pluginProxyRemoteEntry'; import pluginProxyRemotes from './plugins/pluginProxyRemotes'; import { proxySharedModule } from './plugins/pluginProxySharedModule_preBuild'; +import pluginVarRemoteEntry from './plugins/pluginVarRemoteEntry'; import aliasToArrayPlugin from './utils/aliasToArrayPlugin'; import { ModuleFederationOptions, @@ -95,6 +96,7 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] { }, }, ...pluginManifest(), + ...pluginVarRemoteEntry(), ]; } diff --git a/src/plugins/pluginMFManifest.ts b/src/plugins/pluginMFManifest.ts index ad1c6f9..f3b9561 100644 --- a/src/plugins/pluginMFManifest.ts +++ b/src/plugins/pluginMFManifest.ts @@ -1,12 +1,13 @@ import * as path from 'pathe'; -import { Plugin } from 'vite'; import type { PluginContext } from 'rollup'; +import { Plugin } from 'vite'; import { getNormalizeModuleFederationOptions, getNormalizeShareItem, } from '../utils/normalizeModuleFederationOptions'; import { getUsedRemotesMap, getUsedShares } from '../virtualModules'; +import { findRemoteEntryFile } from '../utils/bundleHelpers'; import { buildFileToShareKeyMap, collectCssAssets, @@ -26,7 +27,7 @@ interface BuildFileToShareKeyMapContext { const Manifest = (): Plugin[] => { const mfOptions = getNormalizeModuleFederationOptions(); - const { name, filename, getPublicPath, manifest: manifestOptions } = mfOptions; + const { name, filename, getPublicPath, manifest: manifestOptions, varFilename } = mfOptions; let mfManifestName: string = ''; if (manifestOptions === true) { @@ -104,6 +105,13 @@ const Manifest = (): Plugin[] => { path: '', type: 'module', }, + varRemoteEntry: varFilename + ? { + name: varFilename, + path: '', + type: 'var', + } + : undefined, types: { path: '', name: '' }, globalName: name, pluginVersion: '0.2.5', @@ -151,15 +159,11 @@ const Manifest = (): Plugin[] => { let filesMap: PreloadMap = {}; + const foundRemoteEntryFile = findRemoteEntryFile(mfOptions.filename, bundle); + // First pass: Find remoteEntry file - for (const [_, fileData] of Object.entries(bundle)) { - if ( - mfOptions.filename.replace(/[\[\]]/g, '_').replace(/\.[^/.]+$/, '') === fileData.name || - fileData.name === 'remoteEntry' - ) { - remoteEntryFile = fileData.fileName; - break; // We can break early since we only need to find remoteEntry once - } + if (foundRemoteEntryFile) { + remoteEntryFile = foundRemoteEntryFile; } // Second pass: Collect all CSS assets @@ -224,13 +228,21 @@ const Manifest = (): Plugin[] => { */ function generateMFManifest(preloadMap: PreloadMap) { const options = getNormalizeModuleFederationOptions(); - const { name } = options; + const { name, varFilename } = options; const remoteEntry = { name: remoteEntryFile, path: '', type: 'module', }; + const varRemoteEntry = varFilename + ? { + name: varFilename, + path: '', + type: 'module', + } + : undefined; + // Process remotes const remotes = Array.from(Object.entries(getUsedRemotesMap())).flatMap( ([remoteKey, modules]) => @@ -304,6 +316,7 @@ const Manifest = (): Plugin[] => { }, remoteEntry, ssrRemoteEntry: remoteEntry, + varRemoteEntry, types: { path: '', name: '', diff --git a/src/plugins/pluginVarRemoteEntry.ts b/src/plugins/pluginVarRemoteEntry.ts new file mode 100644 index 0000000..ce08e3e --- /dev/null +++ b/src/plugins/pluginVarRemoteEntry.ts @@ -0,0 +1,132 @@ +import { Plugin } from 'vite'; +import { findRemoteEntryFile } from '../utils/bundleHelpers'; +import { warn } from '../utils/logUtils'; +import { getNormalizeModuleFederationOptions } from '../utils/normalizeModuleFederationOptions'; + +const VarRemoteEntry = (): Plugin[] => { + const mfOptions = getNormalizeModuleFederationOptions(); + const { name, varFilename, filename } = mfOptions; + + let viteConfig: any; + + return [ + { + name: 'module-federation-var-remote-entry', + apply: 'serve', + /** + * Stores resolved Vite config for later use + */ + /** + * Finalizes configuration after all plugins are resolved + * @param config - Fully resolved Vite config + */ + configResolved(config) { + viteConfig = config; + }, + /** + * Configures dev server middleware to handle varRemoteEntry requests + * @param server - Vite dev server instance + */ + configureServer(server) { + server.middlewares.use((req, res, next) => { + if (!varFilename) { + next(); + return; + } + if ( + req.url?.replace(/\?.*/, '') === (viteConfig.base + varFilename).replace(/^\/?/, '/') + ) { + res.setHeader('Content-Type', 'text/javascript'); + res.setHeader('Access-Control-Allow-Origin', '*'); + console.log({ filename }); + res.end(generateVarRemoteEntry(filename)); + } else { + next(); + } + }); + }, + }, + { + name: 'module-federation-var-remote-entry', + enforce: 'post', + /** + * Initial plugin configuration + * @param config - Vite config object + * @param command - Current Vite command (serve/build) + */ + config(config, { command }) { + if (!config.build) config.build = {}; + }, + /** + * Generates the module federation "var" remote entry file + * @param options - Rollup output options + * @param bundle - Generated bundle assets + */ + async generateBundle(options, bundle) { + if (!varFilename) return; + + const isValidName = isValidVarName(name); + + if (!isValidName) { + warn( + `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).` + ); + } + + const remoteEntryFile = findRemoteEntryFile(mfOptions.filename, bundle); + + if (!remoteEntryFile) + throw new Error( + `Couldn't find a remoteEntry chunk file for ${mfOptions.filename}, can't generate varRemoteEntry file` + ); + + this.emitFile({ + type: 'asset', + fileName: varFilename, + source: generateVarRemoteEntry(remoteEntryFile), + }); + }, + }, + ]; + + function isValidVarName(name: string) { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); + } + + /** + * Generates the final "var" remote entry file + * @param remoteEntryFile - Path to esm remote entry file + * @returns Complete "var" remoteEntry.js file source + */ + function generateVarRemoteEntry(remoteEntryFile: string) { + const options = getNormalizeModuleFederationOptions(); + + const { name, varFilename } = options; + + const isValidName = isValidVarName(name); + + // @TODO: implement publicPath/getPublicPath support + return ` + ${isValidName ? `var ${name};` : ''} + ${isValidName ? name : `globalThis['${name}']`} = (function () { + function getScriptUrl() { + const currentScript = document.currentScript; + if (!currentScript) { + console.error("[VarRemoteEntry] ${varFilename} script should be called from sync