Skip to content

Metro Resolver and Node.js Resolve Different package.json Files for the Same Package #1517

Open

Description

@algoORgoal

Issue Description

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

  1. 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
  1. Modified node_modules/msw/native/package.json to use lib2 instead of lib:
{
  "type": "commonjs",
  "browser": null,
  "main": "../lib2/native/index.js",
  "module": "../lib2/native/index.mjs",
  "types": "../lib2/native/index.d.ts",
  "exports": {
    ".": {
      "react-native": {
        "import": {
          "types": "./../lib2/native/index.d.mts",
          "default": "./../lib2/native/index.mjs"
        },
        "default": {
          "types": "./../lib2/native/index.d.ts",
          "default": "./../lib2/native/index.js"
        }
      }
      // ... other exports
    }
  }
}
  1. Test in two environments:
    • Node.js environment: Uses node_modules/msw/package.json
    • Metro bundler environment: Uses node_modules/msw/native/package.json

Expected Behavior

Both Node.js and Metro bundler should resolve the same package.json file for a given package, ensuring consistent behavior across environments.

Actual Behavior

  • Node.js picks up node_modules/msw/package.json
  • Metro bundler picks up node_modules/msw/native/package.json

Impact

This inconsistency can lead to:

  1. Different module resolution paths
  2. Inconsistent behavior between development and production
  3. Potential runtime errors when switching between Node.js and Metro environments

Test Cases

Environment

  • Metro version: v0.82.4
  • Node.js version: v22.15.0
  • React Native version: v0.79.3

Additional Context

This issue was discovered while working with the MSW (Mock Service Worker) package, but it could affect any package with nested package.json files.

Screenshots

[Include the screenshots showing the different outputs from Node.js and Metro]

  • output from node.js
    the output of node.js

  • output from metro
    the output of metro

Activity

  1. changed the title [-]the package res[/-] [+]Metro Resolver and Node.js Resolve Different package.json Files for the Same Package[/+] on Jun 10, 2025
  2. algoORgoal commented on Jun 10, 2025

    @algoORgoal
    Author

    This is derived from mswjs/msw#2523.

    The maintainer of msw, @kettanaito, raises other concerns in the issue, such as:

    1. The bundler doesn't seem to read the export conditions;
    2. 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.

  3. robhogan commented on Jun 10, 2025

    @robhogan
    Collaborator

    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 closest package.json).

    We don't have a fix just yet, but tracking this as a bug.

  4. robhogan commented on Jun 11, 2025

    @robhogan
    Collaborator

    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).

  5. robhogan commented on Jun 11, 2025

    @robhogan
    Collaborator

    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.

  6. shawnthye-guru commented on Sep 5, 2026

    @shawnthye-guru

    Confirming this is still reproducible on newer versions:

    • metro/metro-resolver 0.84.5 / 0.84.6
    • msw 2.15.0 (also present with @mswjs/interceptors 0.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 nested native/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.

  7. Om-singhaI commented on Sep 15, 2026

    @Om-singhaI

    @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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions