diff --git a/.changeset/clean-cobras-rest.md b/.changeset/clean-cobras-rest.md
new file mode 100644
index 00000000000..8e5bcc894e1
--- /dev/null
+++ b/.changeset/clean-cobras-rest.md
@@ -0,0 +1,14 @@
+---
+"@module-federation/runtime-core": patch
+"@module-federation/enhanced": patch
+"@module-federation/rspack": patch
+"@module-federation/sdk": patch
+---
+
+Add import map remote entry support in runtime-core with a tree-shakeable
+`FEDERATION_OPTIMIZE_NO_IMPORTMAP` flag, and expose `disableImportMap`
+in Module Federation plugin optimization options (defaulting to `true`
+in build plugins).
+
+Shared modules now honor the `import` field to load shared dependencies
+via dynamic import (including bare specifiers resolved by import maps).
diff --git a/apps/import-map/README.md b/apps/import-map/README.md
new file mode 100644
index 00000000000..cd4f72e9e65
--- /dev/null
+++ b/apps/import-map/README.md
@@ -0,0 +1,26 @@
+# Import Map Runtime Demo
+
+This example demonstrates using `@module-federation/runtime-core` with an
+import map. The host (app1) loads the remote (app2) by using a bare specifier
+mapped in the import map.
+
+## Running the demo
+
+Start both apps in separate terminals:
+
+```bash
+pnpm --filter import-map-app2 run build
+pnpm --filter import-map-app2 run serve
+
+pnpm --filter import-map-app1 run build
+pnpm --filter import-map-app1 run serve
+```
+
+- Host: http://127.0.0.1:3101
+- Remote entry (import map target): http://127.0.0.1:3102/remoteEntry.js
+
+## E2E
+
+```bash
+pnpm --filter import-map-app1 run test:e2e
+```
diff --git a/apps/import-map/app1/cypress.config.js b/apps/import-map/app1/cypress.config.js
new file mode 100644
index 00000000000..fa2b0d3dbb4
--- /dev/null
+++ b/apps/import-map/app1/cypress.config.js
@@ -0,0 +1,9 @@
+const { defineConfig } = require('cypress');
+
+module.exports = defineConfig({
+ e2e: {
+ specPattern: 'cypress/e2e/**/*.cy.{js,ts,jsx,tsx}',
+ supportFile: 'cypress/support/e2e.ts',
+ },
+ defaultCommandTimeout: 20000,
+});
diff --git a/apps/import-map/app1/cypress/e2e/import-map.cy.ts b/apps/import-map/app1/cypress/e2e/import-map.cy.ts
new file mode 100644
index 00000000000..0288177eacc
--- /dev/null
+++ b/apps/import-map/app1/cypress/e2e/import-map.cy.ts
@@ -0,0 +1,10 @@
+describe('import-map runtime host', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('loads the remote module via import map', () => {
+ cy.get('[data-test="status"]').contains('Loaded');
+ cy.get('[data-test="remote-message"]').contains(
+ 'Hello from import map remote',
+ );
+ });
+});
diff --git a/apps/import-map/app1/cypress/fixtures/example.json b/apps/import-map/app1/cypress/fixtures/example.json
new file mode 100644
index 00000000000..63f783087b5
--- /dev/null
+++ b/apps/import-map/app1/cypress/fixtures/example.json
@@ -0,0 +1,3 @@
+{
+ "name": "import-map"
+}
diff --git a/apps/import-map/app1/cypress/support/commands.ts b/apps/import-map/app1/cypress/support/commands.ts
new file mode 100644
index 00000000000..7d07d532499
--- /dev/null
+++ b/apps/import-map/app1/cypress/support/commands.ts
@@ -0,0 +1,14 @@
+///
+
+// eslint-disable-next-line @typescript-eslint/no-namespace
+declare namespace Cypress {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ interface Chainable {
+ login(email: string, password: string): void;
+ }
+}
+
+Cypress.Commands.add('login', (email, password) => {
+ void email;
+ void password;
+});
diff --git a/apps/import-map/app1/cypress/support/e2e.ts b/apps/import-map/app1/cypress/support/e2e.ts
new file mode 100644
index 00000000000..e58203ee857
--- /dev/null
+++ b/apps/import-map/app1/cypress/support/e2e.ts
@@ -0,0 +1,6 @@
+// ***********************************************************
+// This example support/e2e.ts is processed and
+// loaded automatically before your test files.
+// ***********************************************************
+
+import './commands';
diff --git a/apps/import-map/app1/cypress/tsconfig.json b/apps/import-map/app1/cypress/tsconfig.json
new file mode 100644
index 00000000000..f0a0fad198f
--- /dev/null
+++ b/apps/import-map/app1/cypress/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["cypress", "node"],
+ "sourceMap": false
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.js",
+ "../cypress.config.js",
+ "../**/*.cy.ts",
+ "../**/*.cy.tsx",
+ "../**/*.cy.js",
+ "../**/*.cy.jsx",
+ "../**/*.d.ts"
+ ]
+}
diff --git a/apps/import-map/app1/package.json b/apps/import-map/app1/package.json
new file mode 100644
index 00000000000..af7a3b50cf0
--- /dev/null
+++ b/apps/import-map/app1/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "import-map-app1",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "build": "webpack --config ./webpack.config.js --mode=development",
+ "build:prod": "webpack --config ./webpack.config.js --mode=production",
+ "serve": "node ../serve-static.js --root ./dist --port 3101",
+ "e2e": "cypress run --config-file ./cypress.config.js --config baseUrl=http://127.0.0.1:3101 --browser chrome",
+ "test:e2e": "concurrently -k \"pnpm --filter import-map-app2 run build\" \"pnpm --filter import-map-app1 run build\" \"pnpm --filter import-map-app2 run serve\" \"pnpm --filter import-map-app1 run serve\" \"wait-on http://127.0.0.1:3102/remoteEntry.js http://127.0.0.1:3101 && pnpm --filter import-map-app1 run e2e\""
+ },
+ "devDependencies": {
+ "@module-federation/runtime-core": "workspace:*"
+ }
+}
diff --git a/apps/import-map/app1/src/index.html b/apps/import-map/app1/src/index.html
new file mode 100644
index 00000000000..8b4836542de
--- /dev/null
+++ b/apps/import-map/app1/src/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Import Map Runtime Host
+
+
+
+
+ Import Map Runtime Host
+ Idle
+ Waiting for remote...
+
+
+
diff --git a/apps/import-map/app1/src/index.ts b/apps/import-map/app1/src/index.ts
new file mode 100644
index 00000000000..bdc74cdfbdd
--- /dev/null
+++ b/apps/import-map/app1/src/index.ts
@@ -0,0 +1,48 @@
+import { ModuleFederation } from '@module-federation/runtime-core';
+
+const statusEl = document.querySelector('[data-test="status"]');
+const messageEl = document.querySelector('[data-test="remote-message"]');
+
+const setStatus = (message: string) => {
+ if (statusEl) {
+ statusEl.textContent = message;
+ }
+};
+
+const setMessage = (message: string) => {
+ if (messageEl) {
+ messageEl.textContent = message;
+ }
+};
+
+const federation = new ModuleFederation({
+ name: 'import-map-host',
+ remotes: [
+ {
+ name: 'import_map_remote',
+ entry: 'import_map_remote',
+ type: 'module',
+ entryFormat: 'importmap',
+ },
+ ],
+ shared: {},
+});
+
+const loadRemoteMessage = async () => {
+ try {
+ setStatus('Loading remote...');
+ const remoteModule = await federation.loadRemote<{
+ default?: () => string;
+ }>('import_map_remote/hello');
+
+ const message =
+ remoteModule?.default?.() ?? 'Remote module did not return a message.';
+ setMessage(message);
+ setStatus('Loaded');
+ } catch (error) {
+ setStatus('Failed');
+ setMessage(`Error loading remote: ${String(error)}`);
+ }
+};
+
+void loadRemoteMessage();
diff --git a/apps/import-map/app1/tsconfig.app.json b/apps/import-map/app1/tsconfig.app.json
new file mode 100644
index 00000000000..d5657e0790f
--- /dev/null
+++ b/apps/import-map/app1/tsconfig.app.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "exclude": [
+ "jest.config.ts",
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.js",
+ "**/*.test.js",
+ "dist/**"
+ ],
+ "include": ["**/*.ts"]
+}
diff --git a/apps/import-map/app1/tsconfig.json b/apps/import-map/app1/tsconfig.json
new file mode 100644
index 00000000000..4ee1ce40929
--- /dev/null
+++ b/apps/import-map/app1/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "./",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/apps/import-map/app1/webpack.config.js b/apps/import-map/app1/webpack.config.js
new file mode 100644
index 00000000000..77c68434643
--- /dev/null
+++ b/apps/import-map/app1/webpack.config.js
@@ -0,0 +1,50 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports = {
+ entry: path.resolve(__dirname, 'src/index.ts'),
+ devtool: false,
+ experiments: {
+ outputModule: true,
+ },
+ output: {
+ path: path.resolve(__dirname, 'dist'),
+ filename: 'main.js',
+ publicPath: 'http://127.0.0.1:3101/',
+ module: true,
+ scriptType: 'module',
+ clean: true,
+ },
+ resolve: {
+ extensions: ['.ts', '.js'],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'swc-loader',
+ options: {
+ jsc: {
+ parser: {
+ syntax: 'typescript',
+ },
+ target: 'es2021',
+ },
+ },
+ },
+ },
+ ],
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: path.resolve(__dirname, 'src/index.html'),
+ scriptLoading: 'module',
+ }),
+ ],
+ optimization: {
+ runtimeChunk: false,
+ splitChunks: false,
+ },
+};
diff --git a/apps/import-map/app2/package.json b/apps/import-map/app2/package.json
new file mode 100644
index 00000000000..374938a8c57
--- /dev/null
+++ b/apps/import-map/app2/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "import-map-app2",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "build": "webpack --config ./webpack.config.js --mode=development",
+ "build:prod": "webpack --config ./webpack.config.js --mode=production",
+ "serve": "node ../serve-static.js --root ./dist --port 3102"
+ },
+ "devDependencies": {
+ "@module-federation/enhanced": "workspace:*"
+ }
+}
diff --git a/apps/import-map/app2/src/hello.ts b/apps/import-map/app2/src/hello.ts
new file mode 100644
index 00000000000..0bd2d9a4fc9
--- /dev/null
+++ b/apps/import-map/app2/src/hello.ts
@@ -0,0 +1,3 @@
+const hello = () => 'Hello from import map remote';
+
+export default hello;
diff --git a/apps/import-map/app2/src/index.html b/apps/import-map/app2/src/index.html
new file mode 100644
index 00000000000..9b0aee88294
--- /dev/null
+++ b/apps/import-map/app2/src/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Import Map Remote
+
+
+ Import Map Remote
+
+
diff --git a/apps/import-map/app2/src/index.ts b/apps/import-map/app2/src/index.ts
new file mode 100644
index 00000000000..4944d9a5c95
--- /dev/null
+++ b/apps/import-map/app2/src/index.ts
@@ -0,0 +1,5 @@
+const root = document.getElementById('root');
+
+if (root) {
+ root.textContent = 'Import Map Remote is running.';
+}
diff --git a/apps/import-map/app2/tsconfig.app.json b/apps/import-map/app2/tsconfig.app.json
new file mode 100644
index 00000000000..d5657e0790f
--- /dev/null
+++ b/apps/import-map/app2/tsconfig.app.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "exclude": [
+ "jest.config.ts",
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.js",
+ "**/*.test.js",
+ "dist/**"
+ ],
+ "include": ["**/*.ts"]
+}
diff --git a/apps/import-map/app2/tsconfig.json b/apps/import-map/app2/tsconfig.json
new file mode 100644
index 00000000000..4ee1ce40929
--- /dev/null
+++ b/apps/import-map/app2/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "./",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/apps/import-map/app2/webpack.config.js b/apps/import-map/app2/webpack.config.js
new file mode 100644
index 00000000000..3fc4742bfbf
--- /dev/null
+++ b/apps/import-map/app2/webpack.config.js
@@ -0,0 +1,61 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const {
+ ModuleFederationPlugin,
+} = require('@module-federation/enhanced/webpack');
+
+module.exports = {
+ entry: path.resolve(__dirname, 'src/index.ts'),
+ devtool: false,
+ experiments: {
+ outputModule: true,
+ },
+ output: {
+ path: path.resolve(__dirname, 'dist'),
+ filename: 'main.js',
+ publicPath: 'http://127.0.0.1:3102/',
+ module: true,
+ scriptType: 'module',
+ clean: true,
+ },
+ resolve: {
+ extensions: ['.ts', '.js'],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'swc-loader',
+ options: {
+ jsc: {
+ parser: {
+ syntax: 'typescript',
+ },
+ target: 'es2021',
+ },
+ },
+ },
+ },
+ ],
+ },
+ plugins: [
+ new ModuleFederationPlugin({
+ name: 'import_map_remote',
+ filename: 'remoteEntry.js',
+ library: { type: 'module' },
+ exposes: {
+ './hello': './src/hello.ts',
+ },
+ }),
+ new HtmlWebpackPlugin({
+ template: path.resolve(__dirname, 'src/index.html'),
+ scriptLoading: 'module',
+ }),
+ ],
+ optimization: {
+ runtimeChunk: false,
+ splitChunks: false,
+ },
+};
diff --git a/apps/import-map/serve-static.js b/apps/import-map/serve-static.js
new file mode 100644
index 00000000000..8cd29e3619c
--- /dev/null
+++ b/apps/import-map/serve-static.js
@@ -0,0 +1,64 @@
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { URL } = require('url');
+
+const args = process.argv.slice(2);
+const getArg = (name, fallback) => {
+ const idx = args.indexOf(name);
+ if (idx === -1 || idx + 1 >= args.length) {
+ return fallback;
+ }
+ return args[idx + 1];
+};
+
+const root = getArg('--root', process.cwd());
+const port = Number(getArg('--port', '3000'));
+
+const mimeTypes = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'application/javascript; charset=utf-8',
+ '.css': 'text/css; charset=utf-8',
+ '.json': 'application/json; charset=utf-8',
+ '.svg': 'image/svg+xml',
+ '.png': 'image/png',
+};
+
+const server = http.createServer((req, res) => {
+ const requestUrl = new URL(req.url || '/', `http://${req.headers.host}`);
+ const rawPath =
+ requestUrl.pathname === '/' ? '/index.html' : requestUrl.pathname;
+ const safePath = path.normalize(rawPath).replace(/^(\.\.(\/|\\|$))+/, '');
+ const filePath = path.join(root, safePath);
+
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
+ res.setHeader(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With, content-type, Authorization',
+ );
+
+ if (req.method === 'OPTIONS') {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ fs.readFile(filePath, (err, data) => {
+ if (err) {
+ res.writeHead(404);
+ res.end('Not found');
+ return;
+ }
+ const ext = path.extname(filePath);
+ res.writeHead(200, {
+ 'Content-Type': mimeTypes[ext] || 'application/octet-stream',
+ });
+ res.end(data);
+ });
+});
+
+server.listen(port, () => {
+ // eslint-disable-next-line no-console
+ console.log(`Serving ${root} at http://127.0.0.1:${port}`);
+});
diff --git a/arch-doc/architecture-overview.md b/arch-doc/architecture-overview.md
index 90381b575a2..bed187df04a 100644
--- a/arch-doc/architecture-overview.md
+++ b/arch-doc/architecture-overview.md
@@ -554,6 +554,8 @@ graph TB
style OptCheck fill:#f96,stroke:#333,stroke-width:2px
```
+Import map entry preservation is controlled separately via `FEDERATION_OPTIMIZE_NO_IMPORTMAP`, allowing builds that do not use import maps to tree-shake that logic from runtime-core.
+
### Share Scope Management
```mermaid
diff --git a/arch-doc/runtime-architecture.md b/arch-doc/runtime-architecture.md
index a9552c4a3b6..d9b7dfce3d4 100644
--- a/arch-doc/runtime-architecture.md
+++ b/arch-doc/runtime-architecture.md
@@ -75,6 +75,19 @@ const USE_SNAPSHOT =
When `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` is `true`, snapshot functionality is disabled for smaller bundle sizes.
+Import map entry preservation is controlled by the `FEDERATION_OPTIMIZE_NO_IMPORTMAP` build-time flag:
+
+```typescript
+// Declared in remote/index.ts with DefinePlugin
+declare const FEDERATION_OPTIMIZE_NO_IMPORTMAP: boolean;
+const USE_IMPORTMAP =
+ typeof FEDERATION_OPTIMIZE_NO_IMPORTMAP === 'boolean'
+ ? !FEDERATION_OPTIMIZE_NO_IMPORTMAP
+ : true; // Default to true (enable import map support)
+```
+
+When `FEDERATION_OPTIMIZE_NO_IMPORTMAP` is `true`, import map-specific handling is tree-shaken from runtime-core.
+
```mermaid
classDiagram
class ModuleFederation {
@@ -1037,7 +1050,7 @@ class ViteBundlerRuntime implements BundlerRuntimeIntegration {
### Build-Time Responsibilities
The build-time layer handles:
-- **DefinePlugin Integration**: Defines `FEDERATION_BUILD_IDENTIFIER` and `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` flags
+- **DefinePlugin Integration**: Defines `FEDERATION_BUILD_IDENTIFIER`, `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN`, and `FEDERATION_OPTIMIZE_NO_IMPORTMAP` flags
- **Bundle Generation**: Creates remote entry files and module manifests
- **Static Analysis**: Determines shared dependencies and remote configurations
- **Code Splitting**: Separates remote modules from host bundles
@@ -1057,10 +1070,12 @@ The runtime layer handles:
// Build-time defines these globals, runtime consumes them
declare const FEDERATION_BUILD_IDENTIFIER: string;
declare const FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN: boolean;
+declare const FEDERATION_OPTIMIZE_NO_IMPORTMAP: boolean;
// Runtime uses build-time generated information
const buildId = getBuilderId(); // Reads FEDERATION_BUILD_IDENTIFIER
const useSnapshot = !FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN; // Feature flag
+const useImportMap = !FEDERATION_OPTIMIZE_NO_IMPORTMAP; // Feature flag
// Build-time generates manifest, runtime consumes it
const manifest = await fetch('./federation-manifest.json');
diff --git a/arch-doc/sdk-reference.md b/arch-doc/sdk-reference.md
index ecba49c6060..854356709fa 100644
--- a/arch-doc/sdk-reference.md
+++ b/arch-doc/sdk-reference.md
@@ -109,6 +109,10 @@ interface ModuleFederationPluginOptions {
* Enable optimization to skip snapshot plugin
*/
disableSnapshot?: boolean;
+ /**
+ * Enable optimization to skip import map support in runtime-core
+ */
+ disableImportMap?: boolean;
/**
* Target environment for the build
*/
diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
index f308f576e25..2afa27a2550 100644
--- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
+++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
@@ -105,6 +105,10 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
disableSnapshot;
+ const disableImportMap =
+ experiments?.optimization?.disableImportMap ?? true;
+ definePluginOptions['FEDERATION_OPTIMIZE_NO_IMPORTMAP'] = disableImportMap;
+
// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
if (
experiments?.optimization &&
diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts
index 47c62bf5dac..679d9d2cce0 100644
--- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts
+++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts
@@ -468,6 +468,7 @@ const t = {
type: 'object',
properties: {
disableSnapshot: { type: 'boolean' },
+ disableImportMap: { type: 'boolean' },
target: { enum: ['web', 'node'] },
},
additionalProperties: !1,
@@ -4629,6 +4630,8 @@ function D(
if (
'disableSnapshot' !==
e &&
+ 'disableImportMap' !==
+ e &&
'target' !== e
)
return (
@@ -4664,6 +4667,28 @@ function D(
);
var I = e === c;
} else I = !0;
+ if (I)
+ if (
+ void 0 !==
+ r.disableImportMap
+ ) {
+ const e = c;
+ if (
+ 'boolean' !=
+ typeof r.disableImportMap
+ )
+ return (
+ (D.errors = [
+ {
+ params: {
+ type: 'boolean',
+ },
+ },
+ ]),
+ !1
+ );
+ I = e === c;
+ } else I = !0;
if (I)
if (
void 0 !==
diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json
index 4ca4c940283..7a7d0c26b62 100644
--- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json
+++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json
@@ -883,6 +883,10 @@
"description": "Enable optimization to skip snapshot plugin",
"type": "boolean"
},
+ "disableImportMap": {
+ "description": "Enable optimization to skip import map support in runtime-core",
+ "type": "boolean"
+ },
"target": {
"description": "Target environment for the build",
"enum": ["web", "node"]
diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts
index 4cfba3f43aa..5884764e6dc 100644
--- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts
+++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts
@@ -977,6 +977,11 @@ export default {
description: 'Enable optimization to skip snapshot plugin',
type: 'boolean',
},
+ disableImportMap: {
+ description:
+ 'Enable optimization to skip import map support in runtime-core',
+ type: 'boolean',
+ },
target: {
description: 'Target environment for the build',
enum: ['web', 'node'],
diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts
index 37f12dac7fc..7fc545398b4 100644
--- a/packages/rspack/src/ModuleFederationPlugin.ts
+++ b/packages/rspack/src/ModuleFederationPlugin.ts
@@ -59,6 +59,10 @@ export class ModuleFederationPlugin implements RspackPluginInstance {
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
disableSnapshot;
+ const disableImportMap =
+ experiments?.optimization?.disableImportMap ?? true;
+ definePluginOptions['FEDERATION_OPTIMIZE_NO_IMPORTMAP'] = disableImportMap;
+
// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
if (
experiments?.optimization &&
diff --git a/packages/runtime-core/README.md b/packages/runtime-core/README.md
index 02886810e83..fca384fbdc5 100644
--- a/packages/runtime-core/README.md
+++ b/packages/runtime-core/README.md
@@ -8,6 +8,47 @@
See [https://module-federation.io/guide/runtime/index.html](https://module-federation.io/guide/runtime/index.html) for details.
+## Import Maps
+
+When using Import Maps with `type: "module"` or `type: "system"` remotes, preserve bare specifiers by setting `entryFormat: "importmap"`:
+
+```ts
+import { ModuleFederation } from '@module-federation/runtime-core';
+
+const mf = new ModuleFederation({
+ name: 'host',
+ remotes: [
+ {
+ name: 'webpack_remote',
+ entry: 'webpack_remote',
+ type: 'module',
+ entryFormat: 'importmap',
+ },
+ ],
+});
+```
+
+This keeps the entry untouched so the browser/SystemJS can resolve it via the import map.
+
+To tree-shake import map support, define `FEDERATION_OPTIMIZE_NO_IMPORTMAP` as `true` (or use `experiments.optimization.disableImportMap` when using the ModuleFederationPlugin). Note that the build plugins default `disableImportMap` to `true`, so set it to `false` if you want import map support enabled.
+
+### Shared modules with import maps
+
+You can also load shared modules from import-map specifiers by using the same `import` field naming convention:
+
+```ts
+const mf = new ModuleFederation({
+ name: 'host',
+ shared: {
+ react: {
+ version: '18.3.1',
+ import: 'react',
+ shareConfig: { singleton: true },
+ },
+ },
+});
+```
+
## License
`@module-federation/runtime` is [MIT licensed](https://github.com/module-federation/core/blob/main/packages/runtime/LICENSE).
diff --git a/packages/runtime-core/__tests__/register-remotes.spec.ts b/packages/runtime-core/__tests__/register-remotes.spec.ts
index 174d914c491..1a8953b2e9e 100644
--- a/packages/runtime-core/__tests__/register-remotes.spec.ts
+++ b/packages/runtime-core/__tests__/register-remotes.spec.ts
@@ -102,4 +102,38 @@ describe('ModuleFederation', () => {
// Value is different from the registered remote
expect(newApp1Res).toBe('hello app1 entry2');
});
+
+ it('preserves import map entries when entryFormat is importmap', () => {
+ const entry = 'webpack_remote';
+ const FM = new ModuleFederation({
+ name: '@federation/instance',
+ remotes: [
+ {
+ name: '@register-remotes/importmap',
+ entry,
+ entryFormat: 'importmap',
+ type: 'module',
+ },
+ ],
+ });
+
+ expect(FM.options.remotes[0].entry).toBe(entry);
+ });
+
+ it('normalizes relative entries to absolute urls by default', () => {
+ const entry = '/static/remoteEntry.js';
+ const FM = new ModuleFederation({
+ name: '@federation/instance',
+ remotes: [
+ {
+ name: '@register-remotes/relative',
+ entry,
+ },
+ ],
+ });
+
+ expect(FM.options.remotes[0].entry).toBe(
+ new URL(entry, window.location.origin).href,
+ );
+ });
});
diff --git a/packages/runtime-core/__tests__/shared-import.spec.ts b/packages/runtime-core/__tests__/shared-import.spec.ts
new file mode 100644
index 00000000000..2add2a14dba
--- /dev/null
+++ b/packages/runtime-core/__tests__/shared-import.spec.ts
@@ -0,0 +1,36 @@
+import { assert, describe, expect, it } from 'vitest';
+import { ModuleFederation } from '../src/index';
+
+describe('shared import map support', () => {
+ it('loads shared module from import specifier', async () => {
+ const moduleSource =
+ "export default { message: 'Hello from import map shared module' };";
+ const specifier = `data:text/javascript,${encodeURIComponent(
+ moduleSource,
+ )}`;
+
+ const federation = new ModuleFederation({
+ name: '@import-map/shared-host',
+ remotes: [],
+ shared: {
+ 'import-map-shared': {
+ version: '1.0.0',
+ import: specifier,
+ shareConfig: {
+ singleton: true,
+ requiredVersion: '^1.0.0',
+ eager: false,
+ },
+ },
+ },
+ });
+
+ const factory = await federation.loadShare<{
+ default: { message: string };
+ }>('import-map-shared');
+
+ assert(factory);
+ const mod = factory();
+ expect(mod.default.message).toBe('Hello from import map shared module');
+ });
+});
diff --git a/packages/runtime-core/src/remote/index.ts b/packages/runtime-core/src/remote/index.ts
index 128016dc874..f4e594f2d23 100644
--- a/packages/runtime-core/src/remote/index.ts
+++ b/packages/runtime-core/src/remote/index.ts
@@ -46,6 +46,14 @@ import { formatPreloadArgs, preloadAssets } from '../utils/preload';
import { getGlobalShareScope } from '../utils/share';
import { getGlobalRemoteInfo } from '../plugins/snapshot/SnapshotHandler';
+declare const FEDERATION_OPTIMIZE_NO_IMPORTMAP: boolean;
+const USE_IMPORTMAP =
+ typeof FEDERATION_OPTIMIZE_NO_IMPORTMAP === 'boolean'
+ ? !FEDERATION_OPTIMIZE_NO_IMPORTMAP
+ : true;
+
+const IMPORTMAP_REMOTE_TYPES = new Set(['module', 'system']);
+
export interface LoadRemoteMatch {
id: string;
pkgNameOrAlias: string;
@@ -429,7 +437,23 @@ export class RemoteHandler {
}
// Set the remote entry to a complete path
if ('entry' in remote) {
- if (isBrowserEnv() && !remote.entry.startsWith('http')) {
+ const preserveImportMapEntry =
+ USE_IMPORTMAP && remote.entryFormat === 'importmap';
+ if (
+ preserveImportMapEntry &&
+ !IMPORTMAP_REMOTE_TYPES.has(remote.type || DEFAULT_REMOTE_TYPE)
+ ) {
+ warn(
+ `Remote "${remote.name}" uses entryFormat="importmap" but remote type "${
+ remote.type || DEFAULT_REMOTE_TYPE
+ }" does not support import maps. Use type "module" or "system" to enable import map resolution.`,
+ );
+ }
+ if (
+ isBrowserEnv() &&
+ !preserveImportMapEntry &&
+ !remote.entry.startsWith('http')
+ ) {
remote.entry = new URL(remote.entry, window.location.origin).href;
}
}
diff --git a/packages/runtime-core/src/type/config.ts b/packages/runtime-core/src/type/config.ts
index 98c6c357bbf..e6198a40470 100644
--- a/packages/runtime-core/src/type/config.ts
+++ b/packages/runtime-core/src/type/config.ts
@@ -12,11 +12,14 @@ export type PartialOptional = Omit & {
[P in K]-?: T[P];
};
+export type RemoteEntryFormat = 'url' | 'importmap';
+
export interface RemoteInfoCommon {
alias?: string;
shareScope?: string | string[];
type?: RemoteEntryType;
entryGlobalName?: string;
+ entryFormat?: RemoteEntryFormat;
}
export type RemoteInfoOptionalVersion = {
@@ -40,6 +43,7 @@ export interface RemoteInfo {
type: RemoteEntryType;
entryGlobalName: string;
shareScope: string | string[];
+ entryFormat?: RemoteEntryFormat;
}
export type HostInfo = Pick<
@@ -74,6 +78,7 @@ type SharedBaseArgs = {
strategy?: 'version-first' | 'loaded-first';
loaded?: boolean;
treeShaking?: TreeShakingArgs;
+ import?: string | false;
};
export type SharedGetter = (() => () => Module) | (() => Promise<() => Module>);
@@ -96,6 +101,7 @@ export type Shared = {
lib?: () => Module;
loaded?: boolean;
loading?: null | Promise;
+ import?: string | false;
// compatibility with previous shared
eager?: boolean;
/**
diff --git a/packages/runtime-core/src/utils/load.ts b/packages/runtime-core/src/utils/load.ts
index d3573139aa7..3609156d711 100644
--- a/packages/runtime-core/src/utils/load.ts
+++ b/packages/runtime-core/src/utils/load.ts
@@ -313,5 +313,6 @@ export function getRemoteInfo(remote: Remote): RemoteInfo {
type: remote.type || DEFAULT_REMOTE_TYPE,
entryGlobalName: remote.entryGlobalName || remote.name,
shareScope: remote.shareScope || DEFAULT_SCOPE,
+ entryFormat: remote.entryFormat,
};
}
diff --git a/packages/runtime-core/src/utils/share.ts b/packages/runtime-core/src/utils/share.ts
index 007a7b2ef8e..99d36e52f58 100644
--- a/packages/runtime-core/src/utils/share.ts
+++ b/packages/runtime-core/src/utils/share.ts
@@ -1,5 +1,5 @@
import { DEFAULT_SCOPE } from '../constant';
-import { TreeShakingStatus } from '@module-federation/sdk';
+import { Module, TreeShakingStatus } from '@module-federation/sdk';
import { Global, Federation } from '../global';
import {
GlobalShareScopeMap,
@@ -19,6 +19,30 @@ import { satisfy } from './semver';
import { SyncWaterfallHook } from './hooks';
import { addUniqueItem, arrayOptions } from './tool';
+declare const FEDERATION_OPTIMIZE_NO_IMPORTMAP: boolean;
+const USE_IMPORTMAP =
+ typeof FEDERATION_OPTIMIZE_NO_IMPORTMAP === 'boolean'
+ ? !FEDERATION_OPTIMIZE_NO_IMPORTMAP
+ : true;
+
+const createImportGetter = (specifier: string): SharedGetter => {
+ const dynamicImport = (target: string): Promise => {
+ if (typeof FEDERATION_ALLOW_NEW_FUNCTION !== 'undefined') {
+ return new Function('specifier', 'return import(specifier)')(
+ target,
+ ) as Promise;
+ }
+ return import(
+ /* webpackIgnore: true */
+ /* @vite-ignore */
+ target
+ ) as Promise;
+ };
+
+ return () =>
+ dynamicImport(specifier).then((module) => () => module as Module);
+};
+
function formatShare(
shareArgs: ShareArgs,
from: string,
@@ -31,6 +55,12 @@ function formatShare(
get = shareArgs.get;
} else if ('lib' in shareArgs) {
get = () => Promise.resolve(shareArgs.lib);
+ } else if (
+ USE_IMPORTMAP &&
+ 'import' in shareArgs &&
+ typeof shareArgs.import === 'string'
+ ) {
+ get = createImportGetter(shareArgs.import);
} else {
get = () =>
Promise.resolve(() => {
diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
index 80da0aaa856..b12a6a8b327 100644
--- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
+++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
@@ -263,6 +263,10 @@ export interface ModuleFederationPluginOptions {
* Enable optimization to skip snapshot plugin
*/
disableSnapshot?: boolean;
+ /**
+ * Enable optimization to skip import map support in runtime-core
+ */
+ disableImportMap?: boolean;
/**
* Target environment for the build
*/