diff --git a/examples/vite-vite/vite-host/src/App.jsx b/examples/vite-vite/vite-host/src/App.jsx
index 06da0d3..127a1f2 100644
--- a/examples/vite-vite/vite-host/src/App.jsx
+++ b/examples/vite-vite/vite-host/src/App.jsx
@@ -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 (
@@ -58,6 +65,13 @@ export default function () {
Mfapp01App
+
+
+
+ LazyVarApp
+
+
+
);
}
\ No newline at end of file
diff --git a/examples/vite-vite/vite-host/src/mf.js b/examples/vite-vite/vite-host/src/mf.js
new file mode 100644
index 0000000..2547b30
--- /dev/null
+++ b/examples/vite-vite/vite-host/src/mf.js
@@ -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 };
diff --git a/examples/vite-vite/vite-host/vite.config.js b/examples/vite-vite/vite-host/vite.config.js
index 00c4372..ccd5093 100644
--- a/examples/vite-vite/vite-host/vite.config.js
+++ b/examples/vite-vite/vite-host/vite.config.js
@@ -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: {},
diff --git a/examples/vite-vite/vite-remote/vite.config.js b/examples/vite-vite/vite-remote/vite.config.js
index 92b0aaf..e840df2 100644
--- a/examples/vite-vite/vite-remote/vite.config.js
+++ b/examples/vite-vite/vite-remote/vite.config.js
@@ -28,6 +28,7 @@ export default defineConfig({
'.': './src/App.jsx',
},
filename: 'remoteEntry-[hash].js',
+ varFilename: 'varRemoteEntry.js',
manifest: true,
shared: {
vue: {},
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