Skip to content

feat: add remotePlugin option to control shared module bundling #327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

pcfreak30
Copy link
Contributor

@pcfreak30 pcfreak30 commented Jul 27, 2025

  • Introduces new remotePlugin option to prevent shared modules from being bundled into remotes
  • When enabled, remotes will rely on host-provided shared modules at runtime
  • Updates related virtual modules and plugin logic to respect the new option
  • Conditionally skips hostInit entry addition when remotePlugin is true

I am opening this up as a draft PR for discussion on what this should be named, or how it should be allowed to be configured.

Fundamentally my use case is I use MF as a glorified requirejs module loader without iframes or new web pages.

I have a plugin system where all shared deps are on the host app, as if it was an electron app shell. Thus I have to prevent double bundling of those dependencies... It is also a huge performance bloat to load 5-6 large libraries in every single plugin (remote bundle).

This is part of a series of PR's to upstream my R&D.

- Introduces new `remotePlugin` option to prevent shared modules from being bundled into remotes
- When enabled, remotes will rely on host-provided shared modules at runtime
- Updates related virtual modules and plugin logic to respect the new option
- Conditionally skips hostInit entry addition when remotePlugin is true
Copy link
Collaborator

@gioboa gioboa left a comment

Choose a reason for hiding this comment

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

Thanks for your PR @pcfreak30
I would like to hear the feedback from @ScriptedAlchemy
Please let us know what you think

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Jul 27, 2025

Should be able to accomplish the same thing with import:false on a share package.

Remotes shouldn't be bundling into the remote anyway. So there should be no size increase to require this.

If you want to control module loading and share resolution. Federation runtime plugins are the appropriate way to do this.

In short, the capability should already exist and not require another plugin.

If theres double bundling. Thats a bug in the existing plugin

@pcfreak30
Copy link
Contributor Author

pcfreak30 commented Jul 27, 2025

Should be able to accomplish the same thing with import:false on a share package.

Remotes shouldn't be bundling into the remote anyway. So there should be no size increase to require this.

If you want to control module loading and share resolution. Federation runtime plugins are the appropriate way to do this.

In short, the capability should already exist and not require another plugin.

If theres double bundling. Thats a bug in the existing plugin

As i stands what I have is working perfectly fine. The bundling is because of static references. if you have them in the imports map they will get included even if the share map uses the host. This is a build time issue not a runtime issue....

Also understand I do not pass remotes in at build time. my use case isn't any of the existing examples and I do not have multiple index.html. I use MF as a modern requirejs.

So with the flag I added, i shortcut the imports and prevent them from being bundled while having the prebuild rewrite code reroute them to loadShare.

So yea, this is different then what you are describing I think. and NormalizedModuleFederationOptions has no import property for build time.

@ScriptedAlchemy
Copy link
Member

Okay. So the specification of federation. To treat shared modules as externals who are not bundled in. One would set import: false as a option on the shared object for that library. Which essentially means, dont bundle it and rely only on the parent application to supply it, the get factory would just return throw module not found if remote attempts to use its own getter and not the hosts.

The correct way to implement this would be a false capability on the import option which would then do the reroute to loadShare.

Is this a compiler speed issue? Why not bake in resilience with a few extra chunks? If theres a network issue - the system would have the ability to recover from failure.

Generally I try to encourage users from introducing single point of failures into a distributed system. But since you want fancy requirejs, import: false would be the way to go over adding another plugin, rather update share plugin and options to align with our specifications.

Another important consideration is our runtime is generally designed to expect our specifications. For instance, we may read compiler options in other parts of the ecosystem. For instance deployment layers rely on the spec.

Compile time, import false replaces the getter to the fallback module with a throw error, and uses loadShare to fetch it elsewhere. Runtime doesn't support import false afik since we would just pass a throw as the get() or lib() on the init object - typically thats what the compiler does for us.

@pcfreak30
Copy link
Contributor Author

Okay. So the specification of federation. To treat shared modules as externals who are not bundled in. One would set import: false as a option on the shared object for that library. Which essentially means, dont bundle it and rely only on the parent application to supply it, the get factory would just return throw module not found if remote attempts to use its own getter and not the hosts.

The correct way to implement this would be a false capability on the import option which would then do the reroute to loadShare.

Is this a compiler speed issue? Why not bake in resilience with a few extra chunks? If theres a network issue - the system would have the ability to recover from failure.

Generally I try to encourage users from introducing single point of failures into a distributed system. But since you want fancy requirejs, import: false would be the way to go over adding another plugin, rather update share plugin and options to align with our specifications.

Another important consideration is our runtime is generally designed to expect our specifications. For instance, we may read compiler options in other parts of the ecosystem. For instance deployment layers rely on the spec.

Compile time, import false replaces the getter to the fallback module with a throw error, and uses loadShare to fetch it elsewhere. Runtime doesn't support import false afik since we would just pass a throw as the get() or lib() on the init object - typically thats what the compiler does for us.

If i mark shared as external, then it wont process at all for the loadShare to rewrite in vite/rollup. It must be processed normally, but then re-routed then excluded from the imports.

Its nothing to do with compiler speed, not sure where you got that from? I never mentioned anything about speed. This is a build time problem, and the vite plugin doesn't do anything with the federation runtime library during build other then bundle it?

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Jul 28, 2025

We dont mark it as external. Its simply not added as a dependency and code generation renders a throw on the getter instead and its rerouted to loadShare.

Don't make new plugin. Do same logic, just if shared.react.import = false

Approach is right. Implementation is wrong 🥰

Its nothing to do with compiler speed, not sure where you got that from? I never mentioned anything about speed. This is a build time problem, and the vite plugin doesn't do anything with the federation runtime library during build other then bundle it?

Im still failling to see the point in why you dont want the artifacts to simply exist still?
If it doesnt cause a speed issue, why does it matter if theres an additional js file in the map, it wont load unless the system chooses it - and you can choose who loads it via runtime plugin.

You can say 'only use host shared resources' and still keep the others as fallbacks.

That aside, import: false is the correct way to do it

@ScriptedAlchemy
Copy link
Member

Module Federation remotePlugin PR Analysis - Complete Technical Review

Executive Summary

After examining the LumeWeb/module-federation-vite remote-plugin branch source code and the Module Federation runtime-core implementation, the remotePlugin: true option will cause deterministic runtime failures due to fundamental architectural incompatibilities with the Module Federation runtime's shared module loading mechanism.

PR Implementation Analysis

What the PR Actually Does

The remotePlugin: true option modifies the vite plugin to:

  1. Generate Empty Shared Maps:

    // In virtualRemoteEntry.ts
    const importMap = {
    - ${Array.from(getUsedShares()).map(pkg => `"${pkg}": async () => { ... }`).join(',')}
    + ${options.remotePlugin ? '' : Array.from(getUsedShares()).map(pkg => `"${pkg}": async () => { ... }`).join(',')}
    }
    
    const usedShared = {
    - ${Array.from(getUsedShares()).map(key => `"${key}": { ... }`).join(',')}
    + ${options.remotePlugin ? '' : Array.from(getUsedShares()).map(key => `"${key}": { ... }`).join(',')}
    }
  2. Skip Host Auto-Initialization:

    - ...addEntry({ entryName: 'hostInit', entryPath: getHostAutoInitPath() })
    + ...(remotePlugin ? [] : addEntry({ entryName: 'hostInit', ... }))

    Note: The hostInit entry is added to the host application (when plugin is in host mode), not to individual remotes. This entry handles host-side initialization for consuming remote modules.

  3. Force Host Dependency: Remote should rely entirely on host-provided shared modules instead of bundling its own copies

Author's Use Case

The PR addresses a legitimate architectural pattern:

  • Plugin-based architecture (similar to Electron app shell)
  • Host application provides ALL shared dependencies
  • Remote plugins should not bundle any shared dependencies
  • Remotes should rely entirely on host-provided dependencies at runtime

Runtime Failure Analysis

Critical Issue: Misunderstanding of Shared Module System

After examining the actual virtualRemoteEntry.ts implementation and runtime-core source, the remotePlugin: true approach has a fundamental misunderstanding of how Module Federation's shared module system works:

What remotePlugin: true Actually Does

// From virtualRemoteEntry.ts - generates EMPTY configuration
const importMap = {
  ${options.remotePlugin ? '' : Array.from(getUsedShares()).map(pkg => generateShareImport(pkg)).join(',')}
}

const usedShared = {
  ${options.remotePlugin ? '' : Array.from(getUsedShares()).map(key => generateShareConfig(key)).join(',')}
}

This creates a remote with no shared module declarations whatsoever.

The Fundamental Misunderstanding

Incorrect assumption: "If remote doesn't declare shared dependencies, it will automatically use host's versions"

Reality: Module Federation requires explicit coordination between containers. A remote that doesn't declare its shared dependencies cannot participate in the sharing protocol.

Actual Runtime Flow Analysis

When a consumer (remote or host) tries to access a shared module:

import React from 'react'; // Generated code calls loadShare('react')

Step 1: loadShare() Called on Consumer

// /packages/runtime-core/src/shared/index.ts:111-117
async loadShare<T>(pkgName: string): Promise<false | (() => T | undefined)> {
  const shareInfo = getTargetSharedOptions({
    pkgName,
    shareInfos: host.options.shared, // Consumer's own shared config
  });
  // ...
}

Step 2: getTargetSharedOptions() Looks in Consumer's Config

// /packages/runtime-core/src/utils/share.ts:289-318
export function getTargetSharedOptions(options: {
  shareInfos: ShareInfos; // Consumer's own shared config
}) {
  const defaultResolver = (sharedOptions: ShareInfos[string]) => {
    if (!sharedOptions) {
      return undefined; // ← No config found in consumer
    }
  };
  
  return resolver(shareInfos[pkgName]); // undefined if not declared
}

Step 3: Assertion Failure

// /packages/runtime-core/src/shared/index.ts:152-155
assert(
  shareInfoRes, // ← undefined when consumer has no config
  `Cannot find ${pkgName} Share in the ${host.options.name}.`
);

Key insight: loadShare() first checks the consumer's own shared configuration, not the host's. If the consumer (remote) has no shared config, it fails immediately - it never reaches the host's shared modules.

The Correct Sharing Flow

// How sharing SHOULD work:
// 1. Remote declares what it needs:
const usedShared = {
  react: {
    version: "18.0.0",
    get: async () => import("react"), // Fallback
    shareConfig: { singleton: true, import: false } // Don't bundle, but declare requirement
  }
}

// 2. At runtime, loadShare('react') finds this config
// 3. Runtime can then coordinate with host to get shared version
// 4. If host doesn't provide, falls back to remote's get() function

Why This Approach Cannot Work

The remotePlugin: true approach fundamentally cannot work because:

  1. No shared module protocol participation: Remote cannot participate in version negotiation
  2. No fallback mechanism: Remote has no way to load shared modules when host fails
  3. Runtime architecture mismatch: System expects consumers to declare their requirements
  4. Single point of failure: Complete dependency on host without coordination

Expected vs Actual Behavior

What the author expects:

// Remote with remotePlugin: true
import React from 'react'; // "Should automatically use host's React"

What actually happens:

// Generated remote entry code
const usedShared = {}; // Empty - no React configuration

// At runtime when import is processed:
loadShare('react') 
   getTargetSharedOptions({ shareInfos: {} }) // Empty config
   returns undefined
   assert(undefined) // Throws error
   Application crashes

What should happen (correct approach):

// Remote declares requirement but doesn't bundle
const usedShared = {
  react: {
    shareConfig: { import: false, singleton: true },
    get: () => { throw new Error("Host must provide React") }
  }
}

// At runtime:
loadShare('react')
   getTargetSharedOptions finds react config
   Coordinates with host through share scope
   Returns host's React or throws configured error

Architectural Analysis

Vite Plugin vs Webpack Plugin Difference

Webpack Module Federation:

// import: false still maintains registration
shared: {
  react: {
    import: false,      // Don't bundle locally
    singleton: true,    // Still registered in share scope
    requiredVersion: "^18.0.0"
  }
}
// Runtime knows about the module and can negotiate with host

Vite Plugin with remotePlugin: true:

// Completely empty - no registration at all
const usedShared = {}
// Runtime has no knowledge of any shared modules

The Fundamental Problem

The remotePlugin: true approach violates Module Federation's core contract:

  1. Federation expects coordination: Even if a remote doesn't provide a module, it should declare what it needs
  2. Share scope registration is mandatory: The runtime needs metadata to perform version negotiation
  3. Empty configuration breaks assumptions: The runtime wasn't designed to handle completely unaware remotes
  4. Breaks semantic versioning (semver) sharing: Without version requirements declared by the remote, the host cannot perform proper version negotiation and compatibility checking
  5. Eliminates singleton enforcement: The runtime cannot ensure singleton modules remain singleton without remote participation in the sharing protocol

Comparison: remotePlugin vs import: false

remotePlugin: true Approach

  • Effect: Completely removes shared module declarations from remote
  • Behavior: Crashes immediately on first shared import - cannot participate in sharing protocol
  • Share Scope: No participation - remote is invisible to sharing system
  • Semver Issues: No version coordination possible
  • Runtime: Throws assertion errors in loadShare() before any host coordination
  • Use Case: Intended to force host dependency, but prevents any shared module loading

import: false Approach

  • Effect: Prevents local bundling but maintains federation participation
  • Behavior: Relies on other containers to provide shared modules
  • Share Scope: Still registers metadata (version requirements, singleton settings)
  • Semver Support: Host can perform proper version compatibility checks
  • Singleton Enforcement: Maintains singleton behavior across the federation
  • Runtime: Works correctly with proper error handling for missing modules
  • Use Case: Consume shared modules but don't provide them

Key Runtime Difference

// import: false behavior
loadShare('react') 
  getTargetSharedOptions(): finds react config with import: false 
  Coordinates with share scope to find host's version →
  Returns host's React or fallback

// remotePlugin: true behavior  
loadShare('react') 
  getTargetSharedOptions(): shareInfos is {} (empty) 
  defaultResolver(undefined): returns undefined 
  assert(undefined): THROWS ERROR  application crashes

Working Alternatives

1. Use import: false (Recommended)

// In remote configuration
shared: {
  react: {
    import: false,           // Don't bundle
    singleton: true,         // Use host's version
    requiredVersion: '^18.0.0'
  }
}

Benefits:

  • ✅ Prevents bundling (same as remotePlugin)
  • ✅ Maintains runtime compatibility
  • ✅ Preserves version negotiation
  • ✅ Provides proper error handling
  • ✅ Follows intended Module Federation patterns

2. Maintainer-Recommended Approach: Enhanced import: false

Based on ScriptedAlchemy's feedback in the PR discussion, the correct approach is:

// In remote configuration - modify existing share plugin behavior
shared: {
  react: {
    import: false,           // Don't bundle locally
    singleton: true,         // Use host's version
    requiredVersion: '^18.0.0',
    // Proposed enhancement: throw error on fallback getter
    // Runtime uses loadShare() to fetch from host
  }
}

Maintainer's suggested implementation:

  • Compile-time: Replace getter with throw error when import: false
  • Runtime: Use loadShare() to fetch from host
  • Fallbacks: Optional - can maintain resilience or force host dependency

3. Don't Use Module Federation

If complete isolation is required:

  • Regular ES modules with dynamic imports
  • SystemJS for runtime module loading
  • Custom plugin architecture
  • Micro-frontend frameworks designed for isolation

Ecosystem and Long-term Sustainability Concerns

Beyond the immediate runtime failures, the remotePlugin: true approach introduces significant ecosystem and sustainability risks:

1. Behavioral Deviation from Specification

  • Creates Vite-only behavior: The remotePlugin option would only work in the Vite plugin, not in webpack, rspack, or other bundler implementations
  • No official specification support: This capability is not part of the official Module Federation specification
  • Fragmentation risk: Users would write code that works in Vite but fails in other environments

2. Guaranteed Rolldown Migration Failure

  • Vite is moving to Rolldown: As Vite transitions to Rolldown as its bundler, this feature WILL be dropped
  • All official implementations must adhere to core team specifications: Non-specification features are not permitted in official implementations
  • User regression is inevitable: Users depending on this feature will lose compatibility and end up back in the same scenario they're currently trying to solve

3. Guaranteed Runtime Incompatibility

  • Runtime changes without notice: The Module Federation runtime core may change at any time without considering non-specification behaviors
  • Breaking changes are inevitable: Updates to the runtime WILL break this plugin since it relies on undocumented behavior
  • Zero compatibility guarantees: The runtime team only factors in specification-compliant behaviors when making changes
  • Change requests must go through core repo: Any requests for specification changes must be raised on the core Module Federation repository or risk losing compatibility entirely

4. Maintenance Burden

  • Non-standard implementation: Maintaining behavior that deviates from the specification requires ongoing effort
  • Testing complexity: Need to test against multiple runtime versions and potential breaking changes
  • Documentation gaps: Users would need separate documentation for Vite-specific behavior vs standard Module Federation

Final Verdict

The remotePlugin: true implementation should be rejected due to multiple critical issues:

Technical Problems:

  1. Fundamental architecture misunderstanding - Assumes remotes can consume shared modules without declaring them
  2. Immediate runtime crashes - loadShare() assertions fail when consumer has no shared config
  3. Complete bypass of sharing protocol - Remote cannot participate in version negotiation or coordination
  4. No fallback mechanism - Remote has no way to load shared modules when needed

Ecosystem Problems:

  1. Creates behavioral deviation - Only works in Vite, not other bundler implementations
  2. Not specification-compliant - WILL be dropped during Vite→Rolldown migration (guaranteed)
  3. Runtime compatibility guaranteed failure - WILL break with runtime updates since only specification-compliant behaviors are supported
  4. Maintenance impossible long-term - Cannot maintain non-standard behavior against changing core specifications

Evidence from Source Code:

  • virtualRemoteEntry.ts: Generates completely empty usedShared = {} when remotePlugin: true
  • runtime-core/shared/index.ts: loadShare() expects consumer to have shared configuration
  • runtime-core/utils/share.ts: getTargetSharedOptions() returns undefined for empty shareInfos
  • Assertion logic: System crashes immediately when shareInfo is undefined

Recommendation:

The approach addresses a legitimate use case but is fundamentally based on a misunderstanding of Module Federation's sharing protocol.

Root cause: The author assumes "no shared config = use host's modules" but the reality is "no shared config = cannot participate in sharing at all"

Correct solutions:

  1. Use import: false with proper shared declarations (specification-compliant)
  2. Implement runtime plugin that provides error handling when host fails to provide dependencies
  3. Use alternative architectures if complete isolation is truly required

The remotePlugin: true approach cannot work by design and would cause immediate runtime failures in any real-world usage.

@ScriptedAlchemy
Copy link
Member

Claude more or less confirms my concern with the implementation.

@pcfreak30
Copy link
Contributor Author

pcfreak30 commented Jul 28, 2025

We dont mark it as external. Its simply not added as a dependency and code generation renders a throw on the getter instead and its rerouted to loadShare.

Don't make new plugin. Do same logic, just if shared.react.import = false

Approach is right. Implementation is wrong 🥰

Its nothing to do with compiler speed, not sure where you got that from? I never mentioned anything about speed. This is a build time problem, and the vite plugin doesn't do anything with the federation runtime library during build other then bundle it?

Im still failling to see the point in why you dont want the artifacts to simply exist still? If it doesnt cause a speed issue, why does it matter if theres an additional js file in the map, it wont load unless the system chooses it - and you can choose who loads it via runtime plugin.

You can say 'only use host shared resources' and still keep the others as fallbacks.

That aside, import: false is the correct way to do it

js bloat. i dont want dead code in the bundle at all. There is no reason to have it in bundle. So again, short cutting and skipping the entire import map solves that.

This creates a remote with no shared module declarations whatsoever. Module Federation requires explicit coordination between containers. A remote that doesn't declare its shared dependencies cannot participate in the sharing protocol.

Incorrect for me. The hosts shared deps map get copied to the remote. im manually calling registerRemote and loadRemote in a framework which is after the host has init'ed its own FederationHost. Skipping hostInit might not be needed but I would have to re-test. And even if im using MF in a unique way, ive already built the system, and there is no point in trying to rebuild it. I understand the core fairly deeply now as I have spent a lot of time in a debugger with it.

To be absolutely clear, this change is working for my needs in a development environment.

Your assumptions are based on auto-init of everything from a bundler build for it to auto-wire everything, I think, where as I am taking more low level control of the remotes and managing MF myself.

@pcfreak30
Copy link
Contributor Author

@ScriptedAlchemy
Copy link
Member

hmm, how would it broker shared versions and semver? Based on my understanding, the remove passes absolutely no information at all, not even what it needs for sharing, no version info, singleton flags or requirements?

Are you using loadShare by hand as well? or is that still delegated by the plugin? If you had 2 shares at different versions the remote seems to not have any share information at all, maybe just a vanilla loadRemote('react') and randomly picks one?

regarding js bloat, so we are on the same page - you mean:

shareMap = {
react: {
version:xx,
get() => import()
}}

Thats the bloat?
How many things are you sharing?
Are you using .json remote or .js remote?

Where does your host init itself in your source?

I do want to point out that federation is technically in a broken state in your implementation, if there were ever another module you attach to the system who also shares modules - the application will break.

If you would like the proposed feature, update it to use import: false, then filter the shareMap so that the get() is just a dead function / noop instead of a dynamic import, but the version information and singleton flags are still preserved, basically keep everything but the import() that you dont want.

Alternatively, if you are satisfied with what you have, forking may be a better approach to ensure this remains working after the rolldown switch takes place.

Again, your use case is valid - but supposed to be implemented with import: false like we have in webpack,rspack,metro,rolldown.

These pugins, will they remain under your control only? user will never be able to add their own right?

@pcfreak30
Copy link
Contributor Author

pcfreak30 commented Jul 28, 2025

hmm, how would it broker shared versions and semver? Based on my understanding, the remove passes absolutely no information at all, not even what it needs for sharing, no version info, singleton flags or requirements?

Are you using loadShare by hand as well? or is that still delegated by the plugin? If you had 2 shares at different versions the remote seems to not have any share information at all, maybe just a vanilla loadRemote('react') and randomly picks one?

regarding js bloat, so we are on the same page - you mean:

shareMap = {
react: {
version:xx,
get() => import()
}}

Thats the bloat? How many things are you sharing? Are you using .json remote or .js remote?

Where does your host init itself in your source?

I do want to point out that federation is technically in a broken state in your implementation, if there were ever another module you attach to the system who also shares modules - the application will break.

If you would like the proposed feature, update it to use import: false, then filter the shareMap so that the get() is just a dead function / noop instead of a dynamic import, but the version information and singleton flags are still preserved, basically keep everything but the import() that you dont want.

Alternatively, if you are satisfied with what you have, forking may be a better approach to ensure this remains working after the rolldown switch takes place.

Again, your use case is valid - but supposed to be implemented with import: false like we have in webpack,rspack,metro,rolldown.

These pugins, will they remain under your control only? user will never be able to add their own right?

So, right now im the only author of plugins. I expect that to change in the long future, but the system is very early right now in age. frontend plugins are embedded into a golang plugin (go embed), and served. It works based on how caddy operates in design.

sem ver changes aren't really a concern right now and so I haven't focused or tested on that...

regarding js bloat, so we are on the same page - you mean:

no, the import map.

      
        "@lumeweb/portal-framework-core": async () => {
          let pkg = await import('./index-CGW60ah4.js');
          return pkg
        }
      ,

I am keeping the shared map IIRC. The core ISSUE is that by having let pkg = await import('./index-CGW60ah4.js'); you CANNOT tree shake the dependencies...

const usedShared = {
      
          "@lumeweb/portal-framework-core": {
            name: "@lumeweb/portal-framework-core",
            version: "0.0.0",
            scope: ["default"],
            loaded: false,
            from: "core:dashboard",
            async get () {
              usedShared["@lumeweb/portal-framework-core"].loaded = true;
              const {"@lumeweb/portal-framework-core": pkgDynamicImport} = importMap;
              const res = await pkgDynamicImport();
              const exportModule = {...res};
              // All npm packages pre-built by vite will be converted to esm
              Object.defineProperty(exportModule, "__esModule", {
                value: true,
                enumerable: false
              });
              return function () {
                return exportModule
              }
            },
            shareConfig: {
              singleton: true,
              requiredVersion: "^0.0.0"
            }
          }
        ,

How many things are you sharing?

import type { ModuleFederationOptions } from "@module-federation/vite/lib/utils/normalizeModuleFederationOptions";

const modules = [
  "@lumeweb/portal-framework-core",
  "@lumeweb/portal-framework-ui",
  "@lumeweb/portal-framework-ui-core",
  "@refinedev/core",
  "@tanstack/react-query",
  "react",
  "react-dom",
  "react-router",
  "react-hook-form",
];

export function getSharedModules(): ModuleFederationOptions["shared"] {
  return modules.reduce((acc, module) => {
    acc[module] = {
      eager: true,
      singleton: true,
    };
    return acc;
  }, {}) satisfies ModuleFederationOptions["shared"];
}

Where does your host init itself in your source? https://github.com/LumeWeb/web/blob/f3dc91b58d4282f2e73e9c8b8ac34d7be504952f/go/portal-admin/build/assets/hostInit-4Jn0xzh3.js

The focus is mostly on the remotes behavior. That isn't updated for this patch, but I also didn't change how the host build behaves...

if there were ever another module you attach to the system who also shares modules - the application will break. I have not yet needed to share anything from a plugin, only from the host... will deal with that when needed.

okay yeah, so if you want to get rid og the import(), then import: false pattern on the share and replace the import() with ()=> {console.error('shared fallback module not available')} for cases where imoprpt is false, else import(thedep)

Copy link
Collaborator

@gioboa gioboa left a comment

Choose a reason for hiding this comment

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

Thanks @ScriptedAlchemy for sharing your thoughts and experience here.

this remains working after the rolldown switch takes place.

Do you have an ETA for this?

@ScriptedAlchemy
Copy link
Member

Thanks @ScriptedAlchemy for sharing your thoughts and experience here.

this remains working after the rolldown switch takes place.

Do you have an ETA for this?

we are writing the specs for them now. would have to ask evan

@gioboa
Copy link
Collaborator

gioboa commented Jul 28, 2025

we are writing the specs for them now. would have to ask evan

😅 Yep, of course. Your insight is interesting too, so something is moving in the right direction 👌 Thanks for the update 🤩

@ScriptedAlchemy
Copy link
Member

yes it is. :D

@pcfreak30
Copy link
Contributor Author

@ScriptedAlchemy thoughts on my last response?

@pcfreak30
Copy link
Contributor Author

@gioboa what is your opinion on this PR based on the discussion?

@gioboa
Copy link
Collaborator

gioboa commented Aug 1, 2025

what is your opinion on this PR based on the discussion?

@ScriptedAlchemy shared the mfe team experience on this topic, on the other hand, if your solution is working fine for you, go for it.
From a PR prospective, we should keep the alignment of the codebase with the rest of the module federation systems.

@pcfreak30
Copy link
Contributor Author

pcfreak30 commented Aug 1, 2025

what is your opinion on this PR based on the discussion?

@ScriptedAlchemy shared the mfe team experience on this topic, on the other hand, if your solution is working fine for you, go for it. From a PR prospective, we should keep the alignment of the codebase with the rest of the module federation systems.

And he has yet to respond to my last responses, because I still don't think it has been understood clearly that this is a build time problem, not a runtime problem.

@ScriptedAlchemy
Copy link
Member

After conducting a comprehensive analysis of both the Vite federation codebase and this PR, I need to respectfully disagree with the current implementation. While your use case is absolutely legitimate, the remotePlugin: true approach fundamentally breaks Module Federation's sharing protocol.

The Core Problem

Your current implementation creates an empty share map (usedShared = {}), which destroys Module Federation's ability to:

  • Negotiate versions between containers
  • Enforce singleton behavior
  • Coordinate module loading across federated applications
  • Maintain compatibility with webpack,rspack,rslib,rstest,metro based Module Federation

What You're Trying to Achieve vs. What's Happening

Your Goal (Valid):

// You want: No bundling of shared modules in remotes
// Host provides ALL shared dependencies
// Plugin-style architecture with manual control

Current Implementation (Problematic):

// remotePlugin: true creates:
const usedShared = {}; // Empty - breaks MF protocol

What Should Happen Instead:

const usedShared = {
  "@lumeweb/portal-framework-core": {
    name: "@lumeweb/portal-framework-core",
    version: "0.0.0",
    scope: ["default"],
    loaded: false,
    from: "host",
    async get() {
      // Throw error when not available from host, since theres no local fallback
      throw new Error("Cannot get '@lumeweb/portal-framework-core' - must be provided by host");
    },
    shareConfig: {
      singleton: true,
      requiredVersion: "^0.0.0",
      import: false  // This tells MF theres no local fallback
    }
  }
};

The Root Issue: Vite Federation Lacks import: false Support

After analyzing the Vite federation source code, I discovered the real problem: Vite federation doesn't support import: false at all. This is why you had to resort to the destructive approach.

In webpack Module Federation:

shared: {
  'react': {
    import: false,  // Don't bundle locally, but preserve metadata
    singleton: true,
    requiredVersion: '^18.0.0'
  }
}

In Vite federation (current): This configuration option doesn't exist.

The Proper Solution

Instead of remotePlugin: true, the Vite federation plugin needs to be enhanced to support proper import: false functionality:

1. Add TypeScript Interface Support

// In normalizeModuleFederationOptions.ts
interface SharedConfig {
  import?: string | false;  // Add this missing property
  singleton?: boolean;
  requiredVersion?: string;
  strictVersion?: boolean;
}

2. Modify Virtual Module Generation

// In virtualRemoteEntry.ts - handle import: false case
if (shareItem.shareConfig.import === false) {
  return `
  ${JSON.stringify(key)}: {
    name: ${JSON.stringify(key)},
    version: ${JSON.stringify(shareItem.version)},
    scope: [${JSON.stringify(shareItem.scope)}],
    loaded: false,
    from: "host",
    async get() {
      const shared = await __federation_import_shared__(${JSON.stringify(key)});
      if (shared) return shared;
      throw new Error(\`Shared module '\${${JSON.stringify(key)}}' must be provided by host\`);
    },
    shareConfig: {
      singleton: ${shareItem.shareConfig.singleton},
      requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)},
      import: false
    }
  }`;
}

3. Your Configuration Would Become

// vite.config.js
federation({
  name: 'remote-plugin',
  shared: {
    '@lumeweb/portal-framework-core': {
      import: false,           // Don't bundle locally
      singleton: true,         // Preserve singleton behavior
      requiredVersion: '^0.0.0', // Keep version constraints
    }
  }
})

Why This Matters

Your plugin-based architecture is a legitimate and valuable pattern. However, maintaining Module Federation's coordination protocol ensures:

  1. Future compatibility with other MF implementations
  2. Proper error handling when dependencies are missing
  3. Version negotiation if you ever need it
  4. Debugging support with preserved metadata
  5. Interoperability with webpack,rspack,rslib,rstest,metro based ecosystem

The core insight is: "no bundling" ≠ "no sharing metadata". Module Federation needs the metadata for proper coordination.

Cant merge a pr that simply breaks the sharing protocol.

If you are unwilling implement the feature, then i suggest using rsbuild where the capability already exists.
My response remains unchanged.

@pcfreak30
Copy link
Contributor Author

@ScriptedAlchemy your focusing too much on the usedShared part vs imports map. the shared aspect is less of a concern. The imports map is what caused tree shaking to be impossible and causes bundling. Sure the imports support in usedshared is something to look at, But my biggest concern is not if MF is registering shares twice, but the fact it is bundling them twice... DUE TO the fact its creating an import map and thus can't tree shake.

If removing the imports breaks the sharedmap, well then that is something I would need to look at more...

Oh and im not moving to rspack b/c realistically my whole system uses vite and thats a no-go eng wise. I have things working fine atm, so its just a matter of my changes being tweaked to fit both the standard and my requirements...

Thoughts?

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Aug 4, 2025

I feel the solution i provided you at least 4 times now does what you want. Which is removes the import() from the object and replaces it with a throw etc.

Btw: shared modules should not tree shake in general. Since they are used in unknown ways. But thats a separate discussion from your request.

Import:false, when false dont add a dybamic import in the code generation part. Keep everything else, just not the import() when a share has import false set

@pcfreak30
Copy link
Contributor Author

@ScriptedAlchemy
Copy link
Member

If not then we dont need to add it to the code generation. Just built time option then

@pcfreak30
Copy link
Contributor Author

If not then we dont need to add it to the code generation. Just built time option then

can you please clarify?

@ScriptedAlchemy
Copy link
Member

// In virtualRemoteEntry.ts - handle import: false case
if (shareItem.shareConfig.import === false) {
return ${JSON.stringify(key)}: { name: ${JSON.stringify(key)}, version: ${JSON.stringify(shareItem.version)}, scope: [${JSON.stringify(shareItem.scope)}], loaded: false, from: "host", async get() { const shared = await __federation_import_shared__(${JSON.stringify(key)}); if (shared) return shared; throw new Error(\Shared module '${${JSON.stringify(key)}}' must be provided by host`);
},
shareConfig: {
singleton: ${shareItem.shareConfig.singleton},
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)},
// import: false - remove
}
}`;
}

// vite.config.js
federation({
name: 'remote-plugin',
shared: {
'@lumeweb/portal-framework-core': {
import: false, // Don't bundle locally - KEEP
singleton: true, // Preserve singleton behavior
requiredVersion: '^0.0.0', // Keep version constraints
}
}
})

@ScriptedAlchemy
Copy link
Member

the runtime types themselves dont support / care about import - the compile time plugin types on the other hand, do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants