Resolve a package entry point to a file path or a file path to a package entry point!
This package allows you to resolve a given package entry point (e.g.
mdast-util-from-markdown
in import('mdast-util-from-markdown')
) into a file
path (e.g. ./node_modules/mdast-util-from-markdown/lib/index.js
).
import {
flattenPackageJsonSubpathMap,
resolveExportsTargetsFromEntryPoint
} from 'bidirectional-resolve';
const entrypoint = 'mdast-util-from-markdown';
const { exports: packageJsonExports } = await readJsonFile(
// There are several ways to grab a package's package.json file
`${entrypoint}/package.json`
);
const flatExports = flattenPackageJsonSubpathMap({ map: packageJsonExports });
const nodeModulesPaths = resolveExportsTargetsFromEntryPoint({
flattenedExports: flatExports,
entrypoint,
conditions: ['types', 'require', 'import', 'node']
});
console.log(nodeModulesPaths); // => ['./node_modules/mdast-util-from-markdown/lib/index.js']
This is similar to what is returned by require.resolve
in CJS contexts, or
import.meta.resolve
in ESM contexts, and there are several other libraries
that accomplish some form of this.
What makes bidirectional-resolve
special is that, unlike prior art, it can
also reverse a given file path (e.g.
./node_modules/mdast-util-from-markdown/lib/index.js
) back into an entry point
(e.g. mdast-util-from-markdown
).
import {
flattenPackageJsonSubpathMap,
resolveEntryPointsFromExportsTarget
} from 'bidirectional-resolve';
const precariousNodeModulesImportPath =
'./node_modules/mdast-util-from-markdown/lib/index.js';
const { exports: packageJsonExports } = await readJsonFile(
await packageUp({ cwd: path.dirname(precariousNodeModulesImportPath) })
);
const flatExports = flattenPackageJsonSubpathMap({ map: packageJsonExports });
const entrypoints = resolveEntryPointsFromExportsTarget({
flattenedExports: flatExports,
precariousNodeModulesImportPath,
conditions: ['types', 'require', 'import', 'node']
});
console.log(entrypoints); // => ['mdast-util-from-markdown']
As the above examples demonstrate, bidirectional-resolve
supports
bidirectional conditional resolution of entry points in both exports
and imports package.json
fields.
Deriving a package's entry point from one of its internal file paths satisfies a
variety of use cases. For instance, bidirectional-resolve
can be used to work
around strange behavior in the TypeScript compiler—behavior exhibited since
version 3.9 (2020) and still happening as of 5.7 (2025)—where tsc
sometimes
emits definition files containing relative paths precariously pointing to files
inside the nearest node_modules
directory.
This is not ideal for several reasons, including the fact that package managers
like NPM frequently hoist packages in unpredictable ways, especially in
monorepos, which will silently break these hardcoded import paths. As part of
a post-emit step, bidirectional-resolve
can be used to turn these hardcoded
paths back into their more resilient entrypoint forms.
To install:
npm install bidirectional-resolve
This package exports five functions:
Flattens entry points within a package.json
imports
/exports
map
into a one-dimensional array of subpath-target mappings.
Each resolver function consumes a flattened array of subpath mappings. This function takes the pain out of generating such mappings.
const flattenedExports = flattenPackageJsonSubpathMap({
map: packageJson.exports
});
Given target
and conditions
, this function returns an array of zero or more
entry points that are guaranteed to resolve to target
when the exact
conditions
are active in the runtime. This is done by reverse-mapping target
using exports
from package.json
. exports
is assumed to be valid.
Entry points are sorted in the order they're encountered with the caveat that
exact subpaths always come before subpath patterns. Note that, if target
contains one or more asterisks, the subpaths returned by this function will also
contain an asterisk.
The only other time this function returns a subpath with an asterisk is if the subpath is a "many-to-one" mapping; that is: the subpath has an asterisk but its target does not.
For instance:
{
"exports": {
"many-to-one-subpath-returned-with-asterisk-1/*": "target-with-no-asterisk.js",
"many-to-one-subpath-returned-with-asterisk-2/*": null
}
}
In this case, the asterisk can be replaced with literally anything and it would still match. Hence, the replacement is left up to the caller.
const entrypoints = resolveEntryPointsFromExportsTarget({
flattenedExports,
target,
conditions,
includeUnsafeFallbackTargets,
replaceSubpathAsterisks
});
Given entryPoint
and conditions
, this function returns an array of zero or
more targets that entryPoint
is guaranteed to resolve to when the exact
conditions
are active in the runtime. This is done by mapping entryPoint
using exports
from package.json
. exports
is assumed to be valid.
const targets = resolveExportsTargetsFromEntryPoint({
flattenedExports,
entryPoint,
conditions,
includeUnsafeFallbackTargets
});
Given target
and conditions
, this function returns an array of zero or more
entry points that are guaranteed to resolve to target
when the exact
conditions
are active in the runtime. This is done by reverse-mapping target
using imports
from package.json
. imports
is assumed to be valid.
Entry points are sorted in the order they're encountered with the caveat that
exact subpaths always come before subpath patterns. Note that, if target
contains one or more asterisks, the subpaths returned by this function will also
contain an asterisk.
The only other time this function returns a subpath with an asterisk is if the subpath is a "many-to-one" mapping; that is: the subpath has an asterisk but its target does not.
For instance:
{
"imports": {
"many-to-one-subpath-returned-with-asterisk-1/*": "target-with-no-asterisk.js",
"many-to-one-subpath-returned-with-asterisk-2/*": null
}
}
In this case, the asterisk can be replaced with literally anything and it would still match. Hence, the replacement is left up to the caller.
const entrypoints = resolveEntryPointsFromImportsTarget({
flattenedImports,
target,
conditions,
includeUnsafeFallbackTargets,
replaceSubpathAsterisks
});
Given entryPoint
and conditions
, this function returns an array of zero or
more targets that entryPoint
is guaranteed to resolve to when the exact
conditions
are active in the runtime. This is done by mapping entryPoint
using imports
from package.json
. imports
is assumed to be valid.
const targets = resolveImportsTargetsFromEntryPoint({
flattenedImports,
entryPoint,
conditions,
includeUnsafeFallbackTargets
});
Further documentation can be found under docs/
.
This is a CJS2 package with statically-analyzable exports
built by Babel for use in Node.js versions that are not end-of-life. For
TypeScript users, this package supports both "Node10"
and "Node16"
module
resolution strategies.
Expand details
That means both CJS2 (via require(...)
) and ESM (via import { ... } from ...
or await import(...)
) source will load this package from the same entry points
when using Node. This has several benefits, the foremost being: less code
shipped/smaller package size, avoiding dual package
hazard entirely, distributables are not
packed/bundled/uglified, a drastically less complex build process, and CJS
consumers aren't shafted.
Each entry point (i.e. ENTRY
) in package.json
's
exports[ENTRY]
object includes one or more export
conditions. These entries may or may not include: an
exports[ENTRY].types
condition pointing to a type
declaration file for TypeScript and IDEs, a
exports[ENTRY].module
condition pointing to
(usually ESM) source for Webpack/Rollup, a exports[ENTRY].node
and/or
exports[ENTRY].default
condition pointing to (usually CJS2) source for Node.js
require
/import
and for browsers and other environments, and other
conditions not enumerated here. Check the
package.json file to see which export conditions are
supported.
Note that, regardless of the { "type": "..." }
specified in
package.json
, any JavaScript files written in ESM
syntax (including distributables) will always have the .mjs
extension. Note
also that package.json
may include the
sideEffects
key, which is almost always false
for
optimal tree shaking where appropriate.
See LICENSE.
New issues and pull requests are always welcome and greatly appreciated! 🤩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Or buy me a beer, I'd appreciate it. Thank you!
See CONTRIBUTING.md and SUPPORT.md for more information.
See the table of contributors.