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
Unable to require/import files other than main export in Node #2078
When the "exports" field is defined, all subpaths of the package are encapsulated and no longer available to importers. For example, require('pkg/subpath.js') throws an ERR_PACKAGE_PATH_NOT_EXPORTED error.
I've experienced this in a project where I wanted to import our design tokens.
Steps to reproduce
You can follow these steps to create a barebone test case project:
Create a directory: mkdir cloudfour-export-issue
Enter that directory: cd cloudfour-export-issue
Initialize a new npm project: npm init -y
Install our patterns: npm i -D @cloudfour/patterns
Create a script that attempts to require tokens:printf "const tokens = require(\"@cloudfour/patterns/src/compiled/tokens/json/tokens.json\");" > index.js
Run script: node index
Here is an example of the error you will likely see:
node:internal/modules/cjs/loader:499
throw e;
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './src/compiled/tokens/json/tokens.json' is not defined by "exports" in /PATH/TO/cloudfour-export-issue/node_modules/@cloudfour/patterns/package.json
at new NodeError (node:internal/errors:387:5)
at throwExportsNotFound (node:internal/modules/esm/resolve:464:9)
at packageExportsResolve (node:internal/modules/esm/resolve:748:3)
at resolveExports (node:internal/modules/cjs/loader:493:36)
at Function.Module._findPath (node:internal/modules/cjs/loader:533:31)
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:942:27)
at Function.Module._load (node:internal/modules/cjs/loader:804:27)
at Module.require (node:internal/modules/cjs/loader:1028:19)
at require (node:internal/modules/cjs/helpers:102:18)
at Object.<anonymous> (/Users/tylersticka/Repos/exports-test/index.js:1:16) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
I dug into this a bit, but I'm putting it back because it requires a bit more discussion (and, frankly, a check-in with @calebeby about the right path forward).
My (admittedly shallow) understanding is that exports is the new standard, aiming to replace main and module, and has the capability of conditional exports. However, it also introduces the new side effect that @tylersticka mentioned.
My gut says the right answer is to remove exports in favor of our main and module lines, unless exports is giving us some advantage or feature that I'm missing.
A quick survey of how some other pattern libraries handle this:
You can use glob-like patterns in export maps. It looks like the documentation says that in their glob syntax * matches subpaths as well so in their example an export map like this:
main is standardized in node, but not for importing of esm files from esm files (that was always nonstandard and only implemented by bundlers). module, jsnext:main, es2015, and es2017 are all nonstandard, just implemented by some bundlers and tools.
I think we should stick to export maps and add the globs as needed.
Even package.json is blocked, which trips up tooling that reads a dependency's manifest.
The contradiction
npm pack --dry-run reports 361 files in the tarball. files deliberately publishes /dist, /src/compiled/tokens, /src/**/*.scss, /src/**/*.twig and /src/assets.
Exactly one of those 361 files is reachable through Node resolution. We are deliberately shipping design tokens, Sass partials, Twig templates and SVG assets, and exports then declares all of them private. The two fields disagree about what the public API is, and files is the one expressing the intent.
One nuance that matters for the fix
Sass is not affected the same way. Compiling against the package with loadPaths: ['node_modules'] resolves fine:
That path bypasses Node resolution entirely, so Sass consumers have not been broken and would not notice. It is the JS side — require/import of the JSON and JS token builds, which is exactly what the original report hit — that is blocked. Any fix should keep the Sass route working while unblocking the Node one.
Shape of the fix
Replacing the string with a map of subpaths, roughly:
Widening exports is additive, so it is not a breaking change. The real decision is which of the 361 files we mean to support, because whatever is listed becomes API we are committing to. That decision is closely related to #2427, from the opposite direction: that issue is about three symbols we publish and never intended to expose, this one is about files we intended to expose and cannot reach. Both are the same underlying question of what our public surface actually is, and they are probably worth answering together.
Activity
spaceninja commentedon Dec 16, 2022
I dug into this a bit, but I'm putting it back because it requires a bit more discussion (and, frankly, a check-in with @calebeby about the right path forward).
My (admittedly shallow) understanding is that
exportsis the new standard, aiming to replacemainandmodule, and has the capability of conditional exports. However, it also introduces the new side effect that @tylersticka mentioned.My gut says the right answer is to remove
exportsin favor of ourmainandmodulelines, unlessexportsis giving us some advantage or feature that I'm missing.A quick survey of how some other pattern libraries handle this:
mainandmodulemainmainandmoduleas well ases2015,es2017, andjsnext:mainmainandmodulemaincalebeby commentedon Dec 16, 2022
You can use glob-like patterns in export maps. It looks like the documentation says that in their glob syntax
*matches subpaths as well so in their example an export map like this:would allow importing a file like this:
mainis standardized in node, but not for importing of esm files from esm files (that was always nonstandard and only implemented by bundlers).module,jsnext:main,es2015, andes2017are all nonstandard, just implemented by some bundlers and tools.I think we should stick to export maps and add the globs as needed.
spaceninja commentedon Aug 22, 2026
Still true, and measuring it turned up a contradiction worth recording alongside the original report.
exportsis now a bare string:which encapsulates every subpath. Tested by symlinking the package into a scratch project and resolving as a consumer would:
@cloudfour/patterns@cloudfour/patterns/src/compiled/tokens/json/tokens.jsonERR_PACKAGE_PATH_NOT_EXPORTED@cloudfour/patterns/src/compiled/tokens/scss/_color.scssERR_PACKAGE_PATH_NOT_EXPORTED@cloudfour/patterns/src/components/button/button.scssERR_PACKAGE_PATH_NOT_EXPORTED@cloudfour/patterns/package.jsonERR_PACKAGE_PATH_NOT_EXPORTEDEven
package.jsonis blocked, which trips up tooling that reads a dependency's manifest.The contradiction
npm pack --dry-runreports 361 files in the tarball.filesdeliberately publishes/dist,/src/compiled/tokens,/src/**/*.scss,/src/**/*.twigand/src/assets.Exactly one of those 361 files is reachable through Node resolution. We are deliberately shipping design tokens, Sass partials, Twig templates and SVG assets, and
exportsthen declares all of them private. The two fields disagree about what the public API is, andfilesis the one expressing the intent.One nuance that matters for the fix
Sass is not affected the same way. Compiling against the package with
loadPaths: ['node_modules']resolves fine:That path bypasses Node resolution entirely, so Sass consumers have not been broken and would not notice. It is the JS side —
require/importof the JSON and JS token builds, which is exactly what the original report hit — that is blocked. Any fix should keep the Sass route working while unblocking the Node one.Shape of the fix
Replacing the string with a map of subpaths, roughly:
Widening
exportsis additive, so it is not a breaking change. The real decision is which of the 361 files we mean to support, because whatever is listed becomes API we are committing to. That decision is closely related to #2427, from the opposite direction: that issue is about three symbols we publish and never intended to expose, this one is about files we intended to expose and cannot reach. Both are the same underlying question of what our public surface actually is, and they are probably worth answering together.Related: #2427, #2391.