bidirectional-resolve
TypeScript icon, indicating that this package has built-in type declarations

1.0.2 • Public • Published

Resolve a package entry point to a file path or a file path to a package entry point!


Black Lives Matter! Last commit timestamp Codecov Source license Uses Semantic Release!

NPM version Monthly Downloads


bidirectional-resolve

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.



Install

To install:

npm install bidirectional-resolve

Usage

This package exports five functions:

flattenPackageJsonSubpathMap

API reference

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.

Example

const flattenedExports = flattenPackageJsonSubpathMap({
  map: packageJson.exports
});

resolveEntryPointsFromExportsTarget

API reference

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.

Example

const entrypoints = resolveEntryPointsFromExportsTarget({
  flattenedExports,
  target,
  conditions,
  includeUnsafeFallbackTargets,
  replaceSubpathAsterisks
});

resolveExportsTargetsFromEntryPoint

API reference

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.

Example

const targets = resolveExportsTargetsFromEntryPoint({
  flattenedExports,
  entryPoint,
  conditions,
  includeUnsafeFallbackTargets
});

resolveEntryPointsFromImportsTarget

API reference

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.

Example

const entrypoints = resolveEntryPointsFromImportsTarget({
  flattenedImports,
  target,
  conditions,
  includeUnsafeFallbackTargets,
  replaceSubpathAsterisks
});

resolveImportsTargetsFromEntryPoint

API reference

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.

Example

const targets = resolveImportsTargetsFromEntryPoint({
  flattenedImports,
  entryPoint,
  conditions,
  includeUnsafeFallbackTargets
});

Appendix

Further documentation can be found under docs/.

Published Package Details

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.

License

See LICENSE.

Contributing and Support

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.

Contributors

See the table of contributors.

Package Sidebar

Install

npm i bidirectional-resolve

Weekly Downloads

93

Version

1.0.2

License

MIT

Unpacked Size

50.2 kB

Total Files

11

Last publish

Collaborators

  • xunnamius