Skip to content
Draft
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
14 changes: 14 additions & 0 deletions examples/vite-vite/vite-host/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ import { MuiDemo } from '@namespace/viteViteRemote/MuiDemo';
import StyledDemo from '@namespace/viteViteRemote/StyledDemo';
import { ref } from 'vue';

import { mf } from './mf';

console.log('Share Vue', ref);
console.log('Share React', R, RD);

// @namespace/viteViteRemote is not valid name variable, thus we have to load it with loadRemote instaed of basic usage
const LazyVarApp = R.lazy(() => {
return mf.loadRemote('@namespace/viteViteRemote')
})

export default function () {
return (
<div style={{ background: "lightgray" }}>
Expand Down Expand Up @@ -58,6 +65,13 @@ export default function () {

<h2>Mfapp01App</h2>
<Mfapp01App />

<hr />

<h2>LazyVarApp</h2>
<R.Suspense fallback="loading...">
<LazyVarApp />
</R.Suspense>
</div>
);
}
25 changes: 25 additions & 0 deletions examples/vite-vite/vite-host/src/mf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createInstance } from '@module-federation/runtime';
import React from 'react';

const mf = createInstance({
name: 'viteViteHost',
remotes: [
{
name: '@namespace/viteViteRemote',
entry: 'http://localhost:5176/testbase/varRemoteEntry.js',
type: 'var',
},
],
shared: {
react: {
version: React.version,
lib: () => React,
shareConfig: {
singleton: true,
requiredVersion: false,
},
},
},
});

export { mf };
1 change: 1 addition & 0 deletions examples/vite-vite/vite-host/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default defineConfig({
'@namespace/viteViteRemote': 'http://localhost:5176/testbase/mf-manifest.json',
},
filename: 'remoteEntry-[hash].js',
varFilename: 'varRemoteEntry.js',
manifest: true,
shared: {
vue: {},
Expand Down
1 change: 1 addition & 0 deletions examples/vite-vite/vite-remote/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default defineConfig({
'.': './src/App.jsx',
},
filename: 'remoteEntry-[hash].js',
varFilename: 'varRemoteEntry.js',
manifest: true,
shared: {
vue: {},
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -95,6 +96,7 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
},
},
...pluginManifest(),
...pluginVarRemoteEntry(),
];
}

Expand Down
35 changes: 24 additions & 11 deletions src/plugins/pluginMFManifest.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]) =>
Expand Down Expand Up @@ -304,6 +316,7 @@ const Manifest = (): Plugin[] => {
},
remoteEntry,
ssrRemoteEntry: remoteEntry,
varRemoteEntry,
Copy link
Author

@crutch12 crutch12 Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ScriptedAlchemy wdyt about additional varRemoteEntry field in manifest.json?

Or maybe we should generate the second item in manifest.json array. Sry, I couldn't find the manifest specification...

types: {
path: '',
name: '',
Expand Down
132 changes: 132 additions & 0 deletions src/plugins/pluginVarRemoteEntry.ts
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fail if provided name is not valid js name?
In this case it's not possible to load this remote entry in old fashion way, it won't work:

remotes: {
  '@app/remote': '@app/remote@http://localhost:3000/varRemoteEntry.js',
}

But, as my example shows, this still works with dynamic loadRemote, therefore I've decided to handle it with globalThis and showing the warn message

`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
Copy link
Author

@crutch12 crutch12 Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't handle publicPath/getPublicPath here. Should I? I rely on generated remoteEntry.js relative path

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 <script> tag (document.currentScript is undefined)")
Copy link
Author

@crutch12 crutch12 Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this logic. Maybe there is another way to indicate path of original remoteEntry.js file?
I've stolen it from __webpack_public_path__ ('auto') implementation...

return '/';
}
return document.currentScript.src.replace(/\\/[^/]*$/, '/');
}

const entry = getScriptUrl() + '${remoteEntryFile}';

return {
get: (...args) => import(entry).then(m => m.get(...args)),
init: (...args) => import(entry).then(m => m.init(...args)),
};
})();
`;
}
};

export default VarRemoteEntry;
12 changes: 12 additions & 0 deletions src/utils/bundleHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { OutputBundle } from 'rollup';

export function findRemoteEntryFile(filename: string, bundle: OutputBundle) {
for (const [_, fileData] of Object.entries(bundle)) {
if (
filename.replace(/[\[\]]/g, '_').replace(/\.[^/.]+$/, '') === fileData.name ||
fileData.name === 'remoteEntry'
) {
return fileData.fileName; // We can return early since we only need to find remoteEntry once
}
}
}
6 changes: 6 additions & 0 deletions src/utils/normalizeModuleFederationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ export type ModuleFederationOptions = {
ignoreOrigin?: boolean;
virtualModuleDir?: string;
hostInitInjectLocation?: HostInitInjectLocationOptions;
/**
* Allows generate additional remoteEntry file for "var" host environment
*/
varFilename?: string;
};

export interface NormalizedModuleFederationOptions {
Expand Down Expand Up @@ -347,6 +351,7 @@ export interface NormalizedModuleFederationOptions {
* When true, all CSS assets are bundled into every exposed module.
*/
bundleAllCSS: boolean;
varFilename?: string;
}

type HostInitInjectLocationOptions = 'entry' | 'html';
Expand Down Expand Up @@ -439,5 +444,6 @@ export function normalizeModuleFederationOptions(
virtualModuleDir: options.virtualModuleDir || '__mf__virtual',
hostInitInjectLocation: options.hostInitInjectLocation || 'html',
bundleAllCSS: options.bundleAllCSS || false,
varFilename: options.varFilename,
});
}