Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: resolve workspace packages with dual ESM/CJS exports to ESM entry#860

Merged
gioboa merged 2 commits into
module-federation:mainfrom
hangtiancheng:fix/workspace-dual-format-cjs-resolution
Jun 26, 2026
Merged

fix: resolve workspace packages with dual ESM/CJS exports to ESM entry#860
gioboa merged 2 commits into
module-federation:mainfrom
hangtiancheng:fix/workspace-dual-format-cjs-resolution

Conversation

@hangtiancheng

Copy link
Copy Markdown
Contributor

fix: resolve workspace packages with dual ESM/CJS exports to ESM entry in browser context

Problem

When a workspace package provides dual ESM/CJS exports via the exports field in its package.json:

{
  "type": "module",
  "main": "dist/index.cjs",
  "module": "dist/index.js",
  "exports": {
    ".": {
      "import": { "default": "./dist/index.js" },
      "require": { "default": "./dist/index.cjs" }
    }
  }
}

and this package is shared between a host and a remote via @module-federation/vite (e.g. { "@whoami.js/what": { singleton: true } }), the browser throws at runtime:

Uncaught (in promise) ReferenceError: module is not defined
    at index.cjs:76:1

The offending line in index.cjs is the CommonJS export wrapper emitted by the bundler:

module.exports = __toCommonJS(index_exports);

This is a CommonJS statement executing in the browser, where module does not exist.

Root Cause

The @module-federation/vite plugin needs to determine the on-disk file path of each shared package in order to generate the import statements in its virtual modules. Three functions in src/virtualModules/virtualShared_preBuild.ts use Node.js createRequire().resolve(pkg) for this purpose:

  1. tryResolveImportFromPackageRoot -- walks up from the project root to find where a package is installed. Its return value feeds into getConcreteSharedImportSource and ultimately into the import * as __mfLocalShare from "<path>" statements emitted by generateEagerWorkspaceSingletonExports and generateLazyWorkspaceSingletonExports.

  2. getLocalProviderImportPath -- determines whether a package is a workspace package (symlinked, not in node_modules) and returns its entry path. This path is used as the lazyLocalFallbackSource in writeLoadShareModule, which generates the static import statement for workspace singletons.

  3. getProjectResolvedImportPath -- resolves a package from the project root. Its return value feeds into getDirectSharedCacheSeedImportPath (in virtualRemoteEntry.ts), which generates dynamic import() calls in the localSharedImportMap virtual module.

The problem is that createRequire().resolve(pkg) is the Node.js CommonJS resolver. It uses the default conditions ["node", "require"], which matches the require condition in the exports field:

exports["."].require.default --> "./dist/index.cjs"

For packages in node_modules, this is harmless because Vite's optimizeDeps (dependency pre-bundling) transparently converts CJS to ESM at dev time. But for workspace packages, the pre-bundling step is skipped -- their resolved path is written directly into generated import statements. The browser then loads the .cjs file, encounters module.exports, and crashes.

Fix

Add a resolveWorkspaceEsmEntry() helper in src/virtualModules/virtualShared_preBuild.ts that intercepts createRequire-resolved paths for workspace packages and re-resolves them using ESM-preferring conditions:

function resolveWorkspaceEsmEntry(pkg: string, resolved: string): string {
  if (!isWorkspaceFilePath(resolved)) return resolved;
  const esmEntry = getInstalledPackageEntry(pkg, {
    conditions: ["browser", "import", "module", "default"],
    resolveSubpathWithRequire: false,
  });
  if (esmEntry && isWorkspaceFilePath(esmEntry)) return esmEntry;
  return resolved;
}

The function works as follows:

  1. Non-workspace packages pass through unchanged. If the resolved path is inside node_modules, the original CJS-resolved path is returned as-is. Vite's dependency pre-bundling handles the CJS-to-ESM conversion for these packages, so no intervention is needed.

  2. Workspace packages are re-resolved with ESM conditions. The conditions ['browser', 'import', 'module', 'default'] match exports["."].import.default, which returns the ESM entry (dist/index.js). The resolveSubpathWithRequire: false flag prevents the fallback to createRequire for subpath exports, ensuring consistent ESM resolution.

  3. Graceful fallback. If the ESM re-resolution fails or produces a non-workspace path, the original resolved path is returned. This ensures no regression for edge cases (e.g. packages without an exports field, or workspace packages that only ship CJS).

The helper is applied at the three call sites where createRequire().resolve() results end up in browser code:

// tryResolveImportFromPackageRoot
return resolveWorkspaceEsmEntry(pkg, projectRequire.resolve(pkg));

// getLocalProviderImportPath
const resolved = resolveWorkspaceEsmEntry(pkg, projectRequire.resolve(pkg));

// getProjectResolvedImportPath
return resolveWorkspaceEsmEntry(pkg, projectRequire.resolve(pkg));

Files Changed

File Change
src/virtualModules/virtualShared_preBuild.ts Add resolveWorkspaceEsmEntry() helper; wrap createRequire().resolve() calls in 3 functions
src/virtualModules/__tests__/virtualShared_preBuild.test.ts Add workspace-dual-format mock package and test case

Test Coverage

A new test case workspace-dual-format is added with the following mock setup:

  • createRequire().resolve("workspace-dual-format") returns the CJS entry (/repo/packages/workspace-dual-format/dist/index.cjs)
  • getInstalledPackageEntry with ESM conditions returns the ESM entry (/repo/packages/workspace-dual-format/dist/index.js)
  • getInstalledPackageJson finds the workspace package.json with dual exports

The test asserts that writeLoadShareModule generates code referencing the ESM entry (/dist/index.js) and never the CJS entry (/dist/index.cjs).

All 578 tests pass (including the new one).

Impact

  • Scope: Only affects workspace packages with dual ESM/CJS exports. Packages in node_modules are unaffected.
  • Behavior change: Workspace packages shared via Module Federation will now use their ESM entry point in generated browser code instead of the CJS entry point.
  • Backward compatibility: No breaking changes. Workspace packages that only ship ESM (the common case) already resolved correctly. Workspace packages that only ship CJS will fall back to the original behavior since the ESM re-resolution will fail gracefully.

Reproduction

This bug reproduces in any pnpm/npm/yarn workspace monorepo where:

  1. A workspace package has exports with both import and require conditions pointing to different files (.js vs .cjs)
  2. The package is listed in shared in a @module-federation/vite configuration
  3. The app is run in dev mode (vite dev)

When a workspace package has dual ESM/CJS exports (e.g. exports with both
import and require conditions), createRequire().resolve() follows CJS
conditions and returns the .cjs path. This CJS path then gets emitted in
generated browser code as `import * as __mfLocalShare from "...cjs"`,
causing 'module is not defined' at runtime.

Add resolveWorkspaceEsmEntry() helper that re-resolves workspace package
paths via getInstalledPackageEntry with ESM-preferring conditions
(browser, import, module, default). Apply it in three functions whose
return values end up in browser code:

- tryResolveImportFromPackageRoot
- getLocalProviderImportPath
- getProjectResolvedImportPath

Non-workspace packages (from node_modules) are returned unchanged, so
this only affects workspace packages that produce browser code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@module-federation/vite@860

commit: 1f38860

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tests With Different Frameworks

Image

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @hangtiancheng for your great help πŸ‘

@gioboa gioboa merged commit c3d6ed6 into module-federation:main Jun 26, 2026
19 checks passed
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.

2 participants