DEV Community

Arthur031221
Arthur031221

Posted on

Why Node rejects your package import, and a CLI that tells you

exportwhy explaining a rejected import

The error is short: Package subpath './internal' is not defined by "exports". It tells you what failed and nothing about why, and the fix depends on which of a few causes applies.

The causes

When Node rejects an import of a package subpath, it is one of these:

  • The subpath is not a key in the package's exports map.
  • The key exists, but none of its conditions is active for how you load it. An entry with only import fails under require.
  • The target is null, which blocks the subpath on purpose.
  • The target file is missing from the installed package.
  • The package has no exports field, and ESM wants the full file name.

Finding out which one means opening node_modules/<pkg>/package.json and walking the map by hand, with types, import, require, module-sync and default nested a few levels deep.

What exportwhy does

I wrote a small CLI that does that walk. You run it in the project that fails:

npx github:Arthur031221/exportwhy tiny-lib/public
Enter fullscreen mode Exit fullscreen mode

It asks the Node you run it with to resolve the specifier from the current directory, once with import.meta.resolve and once with createRequire, so npm and pnpm layouts behave as they do at runtime. Then it reads the installed package's exports map and explains the result:

import   OK   node_modules/tiny-lib/dist/public.mjs
              exports["./public"].import
require  FAIL ERR_PACKAGE_PATH_NOT_EXPORTED

Why  "./public" matches, but none of its conditions is active for require.
     this entry is import only: load it with import(), or from an ES module.
Enter fullscreen mode Exit fullscreen mode

For a missing key it lists the nearest public subpaths, and it points out when the file exists on disk but exports hides it. For a package without exports it shows the file name ESM needs. It exits 1 when either mode is rejected, and --json gives output you can paste into an issue.

How it stays honest

The verdict always comes from Node. The explanation is my own implementation of the lookup rules from the Node docs, including * patterns and condition arrays. If the explanation and Node disagree, exportwhy prints Node's answer and says the explanation is not confirmed. The test suite builds hand written packages in both npm and pnpm layouts and also runs against real installs.

Limits

It covers Node resolution only. Vite, webpack, TypeScript and Bun can accept or reject the same specifier differently. It is a v0.1, so I expect exports maps in the wild that it explains badly.

If you have one, the output of exportwhy <specifier> --json is the most useful bug report: https://github.com/Arthur031221/exportwhy

Top comments (0)