Skip to content

Exposed entries with the same file base name result in duplicated assets in manifest #4924

Description

@dalafik

Describe the bug

Reproduces on

"@module-federation/rsbuild-plugin": "2.8.0"

If plugin config exposes entry points with the same base name, but different extensions, like these:

exposes: {
  '.': './src/index.tsx',
  './css': './src/index.css',
}

then resulting mf-manifest.json will contain duplicated entries in exposes/assets fields:

"exposes": [
  {
    "id": "App:.",
    "name": ".",
    "assets": {
      "js": {
        "sync": [
          "static/js/async/__federation_expose_default_export.js",
          "static/js/async/__federation_expose_css.js"
        ],
        "async": []
      },
      "css": {
        "sync": [
          "static/css/async/__federation_expose_default_export.css",
          "static/css/async/__federation_expose_css.css"
        ],
        "async": []
      }
    },
    "path": "."
  },
  {
    "id": "App:css",
    "name": "css",
    "assets": {
      "js": {
        "sync": [
          "static/js/async/__federation_expose_default_export.js",
          "static/js/async/__federation_expose_css.js"
        ],
        "async": []
      },
      "css": {
        "sync": [
          "static/css/async/__federation_expose_default_export.css",
          "static/css/async/__federation_expose_css.css"
        ],
        "async": []
      }
    },
    "path": "./css"
  }

Here is AI report of the investigation (if it helps):

This is a bug in @module-federation/manifest v2.8.0, not intentional
behavior.

The plugin uses getFileNameWithOutExt() — which strips the file extension — as
a map key when grouping expose chunks to expose entries:

// @module-federation/manifest/dist/utils.js
function getFileNameWithOutExt(str) {
    return str.replace(path.extname(str), '');
}

When both expose inputs share the same filename stem:

"packages/design-system/src/index.ts"  → key: "packages/design-system/src/index"
"packages/design-system/src/index.css" → key: "packages/design-system/src/index"
//                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                              COLLISION — same key, different files

The collision chain:

Step Location What happens
_getModuleAssets() assigns chunk assets to a bucket map StatsManager.js:167–220 Both input paths reduce to the same stem key → assets from both chunks are merged into one bucket
fileExposeKeyMap groups expose names by file stem ContainerManager.js:116–128 Both "." and "./css" map to the same key → Set([".", "./css"])
Final stats generation iterates fileExposeKeyMap StatsManager.js:410–443 For every expose name in the set, pushes the same merged asset list
mf-stats.json "file" field is also wrong ModuleHandler.js:386 The ./css expose overwrites the . expose in exposesMap (same stem key)

The strategy was designed for the case documented in the plugin source:
multiple expose aliases pointing to the same file (e.g.
"." → Button.jsx, "./backup" → Button.jsx). It breaks when two
different files happen to share the same stem (index.ts and index.css).

Reproduction

https://github.com/dalafik/module-federation-bug.git

Used Package Manager

npm

System Info

System:
    OS: Linux 6.11 Ubuntu 24.04.2 LTS 24.04.2 LTS (Noble Numbat)
    CPU: (20) x64 13th Gen Intel(R) Core(TM) i7-13700H
    Memory: 18.82 GB / 30.87 GB
    Container: Yes
    Shell: 5.2.21 - /bin/bash
  Binaries:
    Node: 22.17.1
    Yarn: 1.22.22
    npm: 10.9.2
    pnpm: 9.15.4
    bun: 1.3.13
  Browsers:
    Chrome: 148.0.7778.167
    Firefox: 152.0.5
    Firefox Developer Edition: 152.0.5

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions