fix: resolve workspace packages with dual ESM/CJS exports to ESM entry#860
Merged
gioboa merged 2 commits intoJun 26, 2026
Conversation
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]>
commit: |
gioboa
approved these changes
Jun 26, 2026
gioboa
left a comment
Collaborator
There was a problem hiding this comment.
Thanks @hangtiancheng for your great help π
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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
exportsfield in itspackage.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:The offending line in
index.cjsis the CommonJS export wrapper emitted by the bundler:This is a CommonJS statement executing in the browser, where
moduledoes not exist.Root Cause
The
@module-federation/viteplugin needs to determine the on-disk file path of each shared package in order to generate theimportstatements in its virtual modules. Three functions insrc/virtualModules/virtualShared_preBuild.tsuse Node.jscreateRequire().resolve(pkg)for this purpose:tryResolveImportFromPackageRoot-- walks up from the project root to find where a package is installed. Its return value feeds intogetConcreteSharedImportSourceand ultimately into theimport * as __mfLocalShare from "<path>"statements emitted bygenerateEagerWorkspaceSingletonExportsandgenerateLazyWorkspaceSingletonExports.getLocalProviderImportPath-- determines whether a package is a workspace package (symlinked, not innode_modules) and returns its entry path. This path is used as thelazyLocalFallbackSourceinwriteLoadShareModule, which generates the staticimportstatement for workspace singletons.getProjectResolvedImportPath-- resolves a package from the project root. Its return value feeds intogetDirectSharedCacheSeedImportPath(invirtualRemoteEntry.ts), which generates dynamicimport()calls in thelocalSharedImportMapvirtual module.The problem is that
createRequire().resolve(pkg)is the Node.js CommonJS resolver. It uses the default conditions["node", "require"], which matches therequirecondition in theexportsfield:For packages in
node_modules, this is harmless because Vite'soptimizeDeps(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 generatedimportstatements. The browser then loads the.cjsfile, encountersmodule.exports, and crashes.Fix
Add a
resolveWorkspaceEsmEntry()helper insrc/virtualModules/virtualShared_preBuild.tsthat interceptscreateRequire-resolved paths for workspace packages and re-resolves them using ESM-preferring conditions:The function works as follows:
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.Workspace packages are re-resolved with ESM conditions. The conditions
['browser', 'import', 'module', 'default']matchexports["."].import.default, which returns the ESM entry (dist/index.js). TheresolveSubpathWithRequire: falseflag prevents the fallback tocreateRequirefor subpath exports, ensuring consistent ESM resolution.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
exportsfield, 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:Files Changed
src/virtualModules/virtualShared_preBuild.tsresolveWorkspaceEsmEntry()helper; wrapcreateRequire().resolve()calls in 3 functionssrc/virtualModules/__tests__/virtualShared_preBuild.test.tsworkspace-dual-formatmock package and test caseTest Coverage
A new test case
workspace-dual-formatis added with the following mock setup:createRequire().resolve("workspace-dual-format")returns the CJS entry (/repo/packages/workspace-dual-format/dist/index.cjs)getInstalledPackageEntrywith ESM conditions returns the ESM entry (/repo/packages/workspace-dual-format/dist/index.js)getInstalledPackageJsonfinds the workspacepackage.jsonwith dualexportsThe test asserts that
writeLoadShareModulegenerates 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
exports. Packages innode_modulesare unaffected.Reproduction
This bug reproduces in any pnpm/npm/yarn workspace monorepo where:
exportswith bothimportandrequireconditions pointing to different files (.jsvs.cjs)sharedin a@module-federation/viteconfigurationvite dev)