Skip to content

ESM: package.exports array target not conforming to the ESM specification #400

Description

@Boshen

Repro and research: https://github.com/Boshen/test-esm-exports-array

Pasting the content here:

Resolving package.exports array target

Background

ESM can define an array as the exports target:

{
  "exports": {
    ".": [
      "-bad-specifier-",
      "./non-existent.js",
      "./existent.js"
    ]
  }
}

Given only ./existent.js is on disk, enhanced-resolved will resolve to ./existent.js while Node.js will throw a "./non-existent.js not found" error.

Explanation

The ESM specification states the following:

PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions)
...
3. Otherwise, if target is an Array, then
...
  2. For each item targetValue in target, do
    1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error.

Notice the last line continuing the loop on any Invalid Package Target error..

"Invalid Package Target Error" does not mean "File not Found".

This means the above package.json should yield ./non-existent.js instead of ./existent.js.

The reason for this is that exports is designed to resolve unambiguously without hitting the disk.

Also documented by here:

Node.js's implementation picks the first valid path (without attempting to resolve it) and throws an error if it can't be resolved. Node.js's fallback array is designed for forward compatibility with features (e.g. protocols) that can be syntactically validated:

enhanced-resolved

As documented on the Webpack website:

Instead of providing a single result, the package author may provide a list of results. In such a scenario this list is tried in order and the first valid result will be used.


At the moment of writing, I have yet to find a legitimate use case for this feature,
but the behavior from Webpack will lead to people setting up their exports array for some use cases that can break future compatibility.

We have already seen this with the browser field.


All build tools has been reported with the same problem with the same discussions over and over again, linking to the issues:

  • vite - conforms to spec ✔
  • esbuild - conforms to the spec ✔
  • resolve.exports - conforms to the spec ✔
  • resolve-pkg-maps - conforms to the spec ✔
  • node.js
  • typescript - available in 5.0 with moduleResolution: bundler, does not conform to the spec according to the implementation
  • enhanced-resolve - does not conform to the spec as tested by this repo
  • rspack - undecided

Reproduce

pnpm install
bash test.sh
From Node.js:

node:internal/modules/cjs/loader:528
    throw e;
    ^

Error: Cannot find module '/test-esm-exports-array/non-existent.js'
    at createEsmNotFoundErr (node:internal/modules/cjs/loader:1070:15)
    at finalizeEsmResolution (node:internal/modules/cjs/loader:1063:15)
    at trySelf (node:internal/modules/cjs/loader:522:12)
    at Module._resolveFilename (node:internal/modules/cjs/loader:1025:24)
    at Module._load (node:internal/modules/cjs/loader:901:27)
    at Module.require (node:internal/modules/cjs/loader:1115:19)
    at require (node:internal/modules/helpers:130:18)
    at [eval]:1:1
    at Script.runInThisContext (node:vm:122:12)
    at Object.runInThisContext (node:vm:298:38) {
  code: 'MODULE_NOT_FOUND',
  path: '/test-esm-exports-array/package.json'
}

Node.js v20.5.1

----------------------------------------
From esbuild:

✘ [ERROR] Could not resolve "test-esm-exports-array"

    <stdin>:1:7:
      1 │ import('test-esm-exports-array')
        ╵        ~~~~~~~~~~~~~~~~~~~~~~~~

  The module "./non-existent.js" was not found on the file system:

    package.json:6:6:
      6 │       "./non-existent.js",
        ╵       ~~~~~~~~~~~~~~~~~~~

  You can mark the path "test-esm-exports-array" as external to exclude it from the bundle, which
  will remove this error and leave the unresolved path in the bundle. You can also add ".catch()"
  here to handle this failure at run-time instead of bundle-time.

1 error

----------------------------------------
From enhanced-resolve:

dir: /test-esm-exports-array
specifier: test-esm-exports-array
resolved:  /test-esm-exports-array/existent.js

Notice enhanced-resolve resolved to ./existent.js, spec compliant implementations reports ./non-existent.js not found.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions