You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Metro Resolver and Node.js Resolve Different package.json Files for the Same Package #1517
There appears to be an inconsistency between how Metro bundler and Node.js resolve package.json files for the same package. This can lead to different behavior depending on the runtime environment.
Reproduction Steps
Set up a package with nested package.json files:
node_modules/msw/
├── lib/
│ └── [original files]
├── lib2/
│ └── [copied files]
├── native/
│ └── package.json # Modified to use lib2
└── package.json # using lib
Modified node_modules/msw/native/package.json to use lib2 instead of lib:
The maintainer of msw, @kettanaito, raises other concerns in the issue, such as:
The bundler doesn't seem to read the export conditions;
The bundler complains that a valid export condition in a file-based resolution is wrong (and says it will use a file-based resolution instead while it's already using it).
But apparently all of the issues are coming from metro resolver resolving different package.json than node.js.
Hi @algoORgoal - huge thanks for the detail and repro. This does look like a legitimate bug where we're looking for the closest package.json rather than the one at the package root, which is the specified behaviour for ESM resolution - though some investigation is needed to find how that intersects with browser spec resolution in other bundlers (which does look at closestpackage.json).
We don't have a fix just yet, but tracking this as a bug.
Thanks for working on this! getClosestPackage should return the closest package*, from memory I think the change needed would be somewhere around PackageExportsResolve where we should be looking for the package.json at the package root instead of the closest one - but only if the specifier is a packageish (i.e, not a relative or absolute path).
*The closest package.json is still necessary for other types of resolution, like imports (closest to the origin module) and browser (closest to the target path).
Since file system utilities don't support the opposite of this._fileSystem.hierarchicalLookup(), manual js implementaion would be inevitable.
I think the solution would look something like “if the specifier is of the form <packageName>[/<subpath>] , perform a hierarchical lookup for <packageName>/package.json”
I think we’ve looked at this before and it’s not a trivial change because Metro converts the specifier to absolute candidate paths quite early, which loses the information you’d need to subtract the subpath. Not impossible though, we’d certainly welcome a fix.
Confirming this is still reproducible on newer versions:
metro/metro-resolver0.84.5 / 0.84.6
msw2.15.0 (also present with @mswjs/interceptors0.41.9's fetch, XMLHttpRequest, and WebSocket subpath shims — same root cause, different package)
The earlier workaround discussed in mswjs/msw#2523 (stripping the leading ./ from the ..-containing target, e.g. "./../lib/native/index.mjs" → "../lib/native/index.mjs") no longer works — PackageExportsResolve now also rejects it with a different message:
Reason: One or more mappings for subpaths defined in "exports" are invalid. All values must begin with "./". Falling back to file-based resolution.
So there's no longer any exports target string that can satisfy both the "must start with ./" check and the "must not contain .. segments" check for a target that legitimately needs to point outside the shim's own subdirectory (as msw/native/package.json does, since it's a nested/legacy package.json shim one level below the actual lib/ output).
As a temporary workaround in our monorepo, we patched the affected shim package.json files (msw/native, @mswjs/interceptors/fetch, .../XMLHttpRequest, .../WebSocket) to simply remove the invalid exports field, relying on the existing valid main/module/types fields for resolution instead. This silences the warning entirely (verified via a real expo start + bundle request, no warnings, correct module resolved) since Metro only runs PackageExportsResolve when an exports field is present at all.
This still points to the underlying bug identified here: Metro resolves the nestednative/package.json (whose own exports boundary can't legitimately reference files outside itself) instead of resolving the root msw/package.json's ./native export condition, as Node.js ESM resolution would. Fixing that root-cause mismatch (rather than patching every affected library) would make packages like msw that use this common "flat subpath directory" publishing pattern work without needing broken/tricked exports fields in the shims.
@robhogan I've got a fix for this that follows your suggestion. The package root is passed down from resolveFromNodeModulesPath (and the extraNodeModules and Haste paths), and exports are read from there, while getPackageForModule keeps returning the closest package.
One catch before I open it. It changes which build React Native gets for some packages, including the one in this issue. msw/native imports @mswjs/interceptors/fetch, and that package's root exports have no react-native condition. Today Metro picks the browser build through the nested browser field. With the fix it picks the Node build, which imports node:zlib, so msw/native stops bundling.
Would you want it behind an unstable_ resolver flag, off by default, for now?
Activity
[-]the package res[/-][+]Metro Resolver and Node.js Resolve Different package.json Files for the Same Package[/+]algoORgoal commentedon Jun 10, 2025
This is derived from mswjs/msw#2523.
The maintainer of msw, @kettanaito, raises other concerns in the issue, such as:
But apparently all of the issues are coming from metro resolver resolving different package.json than node.js.
robhogan commentedon Jun 10, 2025
Hi @algoORgoal - huge thanks for the detail and repro. This does look like a legitimate bug where we're looking for the closest
package.jsonrather than the one at the package root, which is the specified behaviour for ESM resolution - though some investigation is needed to find how that intersects with browser spec resolution in other bundlers (which does look at closestpackage.json).We don't have a fix just yet, but tracking this as a bug.
robhogan commentedon Jun 11, 2025
Thanks for working on this!
getClosestPackageshould return the closest package*, from memory I think the change needed would be somewhere aroundPackageExportsResolvewhere we should be looking for thepackage.jsonat the package root instead of the closest one - but only if the specifier is a packageish (i.e, not a relative or absolute path).*The closest
package.jsonis still necessary for other types of resolution, likeimports(closest to the origin module) andbrowser(closest to the target path).robhogan commentedon Jun 11, 2025
I think the solution would look something like “if the specifier is of the form
<packageName>[/<subpath>], perform a hierarchical lookup for<packageName>/package.json”I think we’ve looked at this before and it’s not a trivial change because Metro converts the specifier to absolute candidate paths quite early, which loses the information you’d need to subtract the subpath. Not impossible though, we’d certainly welcome a fix.
shawnthye-guru commentedon Sep 5, 2026
Confirming this is still reproducible on newer versions:
metro/metro-resolver0.84.5/0.84.6msw2.15.0(also present with@mswjs/interceptors0.41.9'sfetch,XMLHttpRequest, andWebSocketsubpath shims — same root cause, different package)The earlier workaround discussed in mswjs/msw#2523 (stripping the leading
./from the..-containing target, e.g."./../lib/native/index.mjs"→"../lib/native/index.mjs") no longer works —PackageExportsResolvenow also rejects it with a different message:So there's no longer any
exportstarget string that can satisfy both the "must start with./" check and the "must not contain..segments" check for a target that legitimately needs to point outside the shim's own subdirectory (asmsw/native/package.jsondoes, since it's a nested/legacypackage.jsonshim one level below the actuallib/output).As a temporary workaround in our monorepo, we patched the affected shim
package.jsonfiles (msw/native,@mswjs/interceptors/fetch,.../XMLHttpRequest,.../WebSocket) to simply remove the invalidexportsfield, relying on the existing validmain/module/typesfields for resolution instead. This silences the warning entirely (verified via a realexpo start+ bundle request, no warnings, correct module resolved) since Metro only runsPackageExportsResolvewhen anexportsfield is present at all.This still points to the underlying bug identified here: Metro resolves the nested
native/package.json(whose ownexportsboundary can't legitimately reference files outside itself) instead of resolving the rootmsw/package.json's./nativeexport condition, as Node.js ESM resolution would. Fixing that root-cause mismatch (rather than patching every affected library) would make packages likemswthat use this common "flat subpath directory" publishing pattern work without needing broken/trickedexportsfields in the shims.Om-singhaI commentedon Sep 15, 2026
@robhogan I've got a fix for this that follows your suggestion. The package root is passed down from
resolveFromNodeModulesPath(and theextraNodeModulesand Haste paths), andexportsare read from there, whilegetPackageForModulekeeps returning the closest package.One catch before I open it. It changes which build React Native gets for some packages, including the one in this issue.
msw/nativeimports@mswjs/interceptors/fetch, and that package's rootexportshave noreact-nativecondition. Today Metro picks the browser build through the nestedbrowserfield. With the fix it picks the Node build, which importsnode:zlib, so msw/native stops bundling.Would you want it behind an
unstable_resolver flag, off by default, for now?