Open-source project
browserify/resolve avatar
browserify/resolve

resolve: the Node require.resolve algorithm as a callable library

Implements the node.js require.resolve() algorithm

797 stars188 forksJavaScriptMIT

At a glance

What is it?
resolve reimplements the Node require.resolve algorithm so that a tool can resolve module paths on behalf of another file, asynchronously or synchronously, with injectable filesystem hooks and explicit control over exports field conditions. It fits bundler and tooling authors who need Node's resolution semantics outside a require call.
Who is it for?
Adopt resolve when you are writing a bundler, a dependency walker or a lint and codemod tool that has to answer the question of where a module lives for a file that is not the one running the code, because that is exactly the gap Node's own require.resolve leaves. Do not adopt it to look up a package from your own code, where the built in require.resolve already does the job with no dependency at all.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Resolving on behalf of a file that is not yours

resolve exists to do one thing: implement the Node `require.resolve()` algorithm so that you can resolve a module path on behalf of a file, both asynchronously and synchronously.

Read that phrasing carefully, because the clause on behalf of a file is the whole reason the library exists. Node's own `require.resolve()` resolves relative to the file that calls it, so it can answer the question for itself and for nobody else. Any tool that walks a dependency tree, bundles a project, rewrites import statements or analyses another package's dependencies needs to answer that question for files it does not own, and each of those files has its own directory position and therefore its own answer. The `basedir` option is the parameter that makes this possible.

The algorithm itself is not a simplification. The options list in the README covers extensions to try, the module directory to search, whether core modules participate in the search, how package.json files are read and transformed, how symlinks are handled, and a long set of controls over how the `exports` field is interpreted. The goal is compatibility with Node's behaviour rather than a cheaper approximation of it.

The package lives in the browserify organisation and is maintained by James Halliday, and the repository carries a CII Best Practices badge alongside the GitHub Actions and coverage badges. The licence is MIT.

For comparison, Node's own resolution is available in two shapes: the built in `require.resolve`, which is synchronous and tied to the calling file, and the asynchronous `import.meta.resolve`, which is tied to the module rather than the file. resolve predates both of those constraints and gives you the calling-file freedom that neither provides, plus the ability to run the whole thing synchronously when a bundler needs a straight line of code.

Two entry points and the exports map behind them

The API is exposed through two subpath exports plus the package root. The README shows all three requires:

js
var resolve = require('resolve');
var async = require('resolve/async');
var sync = require('resolve/sync');

The root export is the asynchronous form, and the synchronous form is available both as its own subpath and as a `.sync` property on the root export, since the README describes the sync require as `require('resolve/sync')` or, equivalently, `require('resolve').sync`.

The manifest shows how the package publishes these. The `exports` map lists the root as an array with an import condition pointing at `./index.mjs` and a default pointing at `./index.js`, plus `./index.js`, then the `./sync` subpath mapped to `./lib/sync.js`, the `./async` subpath mapped to `./lib/async.js`, and `./package.json` exposed for tools that read it. The `main` field points at `index.js`, and the `bin` field exposes a `resolve` executable at `./bin/resolve`.

Two details in that layout are worth noting. The package ships an ESM entry, `index.mjs`, alongside the CommonJS one, so it can be imported as well as required, and the repository carries matching type declaration files, `index.d.ts`, `index.d.mts`, `sync.d.ts` and `async.d.ts`, with a `tsconfig.json` at the root.

The `bin` entry is worth a mention as well. A command line entry point means the resolver is usable as a tool as well as a library, which is convenient when you want to ask the question from a shell during debugging.

The error codes are the real API contract

For both the synchronous and the asynchronous methods, errors may carry any of a fixed set of `err.code` values, and the README enumerates them. This is the part of the interface that a caller has to handle, and it is more precise than a generic failure.

`MODULE_NOT_FOUND` means the given path string could not be resolved to a module. `INVALID_BASEDIR` means the specified `opts.basedir` does not exist, or is not a directory, which is a different fault from a missing module and points at the caller's configuration rather than at the dependency tree. `INVALID_PACKAGE_MAIN` means a `package.json` was encountered with an invalid `main` property, for example one that is not a string.

The `exports` field has three of its own codes. `ERR_PACKAGE_PATH_NOT_EXPORTED` means the requested subpath is not defined in the package's `exports` field, which is the modern equivalent of a failed deep import. `ERR_INVALID_PACKAGE_CONFIG` means the `exports` field contains an invalid target. `INVALID_EXPORTS_CATEGORY` means an invalid `exportsCategory` was specified, which is a caller error rather than a package error.

Distinguishing these is what makes the library usable inside a tool. A bundler that treats every failure as a hard stop cannot report a useful diagnostic, while a tool that can tell a missing module apart from a package that simply does not export the subpath you asked for can explain the problem to the user. The same distinction applies to `INVALID_BASEDIR`, where the fix is on your side.

The asynchronous form also has a documented quirk in `opts.readPackage`: a SyntaxError error argument will be ignored, while all other error arguments are treated as an error. In other words, a package.json that cannot be parsed is treated as absent rather than fatal, which is the tolerant behaviour a resolver needs when walking trees that contain broken files.

exportsCategory, engines and moduleSystem

The most intricate part of the option set concerns the `exports` field, and it is worth understanding before you rely on it.

By default the library does not apply `exports` field semantics at all, because those semantics change between Node versions and a resolver that guesses wrong is worse than one that stays out of the way. You opt in with one of two mutually exclusive options. The README states plainly that `exportsCategory` and `engines` are mutually exclusive and only one can be specified.

`opts.exportsCategory` takes a node-exports-info category string, with the examples `'conditions'`, `'patterns'` and `'pre-exports'`, and resolution then uses the package's `exports` field according to that category's supported conditions and features. Setting it also enables self-reference support, allowing a package to import itself by name, for example `require('my-package')` from within `my-package`.

`opts.engines` instead decides based on Node version semantics. When it is a string such as `'>= 14'`, it is treated as a semver range and mapped to the most restrictive category covering that range. When it is `true`, the `engines.node` field is read from the nearest `package.json` to `basedir`. When it is `false` or omitted, there is no engine based resolution, and setting it to an empty string or a value that is neither a boolean nor a string throws.

Two further options only take effect when one of those two is set. `opts.moduleSystem` is either `'require'` or `'import'` and decides which module system conditions are used, with `require` as the default. The README gives the concrete example: for a package with `exports: { ".": { "import": "./esm.js", "require": "./cjs.js" } }`, resolving with `moduleSystem: 'import'` returns `esm.js` and with `moduleSystem: 'require'` returns `cjs.js`. `opts.conditions` lets you pass an explicit array of condition strings such as `['require', 'node']`, which overrides the conditions that would otherwise be derived from the category.

That last point is where the design shows its care. A tool that resolves for many projects at once may need to know which conditions are in play, and the escape hatch is explicit rather than inferred.

Injecting the filesystem so resolution can be tested

The option list is long because the library lets you replace almost every step of the resolution with your own function. That is unusual for a resolver and it exists for a reason: a tool that resolves modules needs to do it against something other than the local disk.

The hooks are `opts.readFile` for reading files asynchronously, `opts.isFile` to test asynchronously whether a file exists, `opts.isDirectory` to test whether a path exists and is a directory, and `opts.realpath` to resolve a potential symlink to its real path. Together they let you point resolution at a virtual filesystem, an archive, or a fixture directory.

Package handling is equally injectable. `opts.readPackage(readFile, pkgfile, cb)` reads and parses a package.json, `opts.packageFilter(pkg, pkgfile, dir)` transforms the parsed contents before the `main` field is consulted, and `opts.pathFilter(pkg, path, relativePath)` transforms a path within a package and returns a relative path that is joined from the package.json location.

Those two filters are the interesting ones, because they let you change resolution semantics rather than just change where files come from. A tool that needs to follow a `browser` field mapping, or that wants to redirect an entry point, can do so without forking the resolver.

The rest of the options cover the search itself. `opts.basedir` is where to start. `opts.extensions` is the array of file extensions to search in order, and the default is `['.js']`, which is why a resolver used on a TypeScript project needs to be told about `.ts`. `opts.moduleDirectory` is the directory or directories to search recursively, defaulting to `"node_modules"`. `opts.includeCoreModules` set to `false` excludes Node core modules such as `fs` from the search. `opts.preserveSymlinks` leaves the `basedir` unresolved to its real path, which is how Node resolves dependencies when run with the `--preserve-symlinks` flag. `opts.paths` and `opts.packageIterator` are the advanced escape hatches, and the README marks both as things you probably should not use.

Running the examples and installing the package

The repository ships a working example for each mode, and the README shows both. The asynchronous form takes a module id, a `basedir`, and a callback:

js
var resolve = require('resolve/async'); // or, require('resolve')
resolve('tap', { basedir: __dirname }, function (err, res) {
    if (err) console.error(err);
    else console.log(res);
});

The synchronous form returns the path directly:

js
var resolve = require('resolve/sync'); // or, `require('resolve').sync
var res = resolve('tap', { basedir: __dirname });
console.log(res);

Running either example prints an absolute path to a resolved file, in the README's case a path into a `tap` package under `node_modules`. The `example/` directory holds `async.js` and `sync.js` for exactly this, and the `bin/resolve` entry point offers the same capability from a shell.

The defaults are worth writing down, because most of the surprising behaviour comes from them:

js
{
    paths: [],
    basedir: __dirname,
    extensions: ['.js'],

The `basedir` default is `__dirname`, which in a bundled file is the directory of the bundle rather than the directory of the file you are resolving for. That is a frequent source of confusion, and it is also why the `basedir` option is the one you will pass explicitly in almost every call.

Because the package is on npm, installation is a normal dependency add, and because the module has a `bin` field, it can be run directly from a local install as well.

A test suite that resolves against fixtures, not npm

The repository shows how seriously the resolution behaviour is pinned down, and the test setup explains a good deal about the design.

Tests run with `tape` under `nyc` for coverage, with the lint step running first as a `pretest`. After the unit tests, a `posttest` step runs a multi-repository test and then an `npx npm audit --production` against the production dependency tree, so a known vulnerability in a runtime dependency fails the test run rather than waiting for a release.

The multi-repository test does `npm install` and `npm test` inside `test/resolver/multirepo`, which is a fixture that installs a real dependency tree rather than mocking it. The nested symlinks fixture is prepared by a pre-test step that runs `node mylib/sync` and `node mylib/async` inside it, so both code paths are exercised against a directory of symlinks.

There is a git submodule involved. The `submodule:update` script initialises submodules with a shallow clone and then uses a sparse checkout with cone mode to pull only the `packages/tests` directory from the `test/list-exports` submodule. That fixture exists to check the package's own `exports` map, and the sparse checkout keeps the amount of data fetched to a minimum.

The lint pipeline is broader than the unit tests. `eslint.` runs as the lint step, and the post-lint step runs `tsc -p.` followed by `attw -P`, which checks the package's type declarations against the ways consumers might import them. The packaging step uses `npmignore` with an autogenerated comment filter, and the publish path runs `safe-publish-latest`.

One thing to check before you install. The version in the repository manifest is `2.0.0-next.7`, a prerelease, so check which version you are actually installing. The repository publishes no GitHub releases, so the npm registry is the place where the version history lives. The last push was on 2026-10-06 and the repository is not archived.

Editorial conclusion

Adopt resolve when you are writing a bundler, a dependency walker or a lint and codemod tool that has to answer the question of where a module lives for a file that is not the one running the code, because that is exactly the gap Node's own require.resolve leaves. Do not adopt it to look up a package from your own code, where the built in require.resolve already does the job with no dependency at all. Verify first which version you are installing, since the manifest in the repository is a prerelease at 2.0.0-next.7, and then decide deliberately how you want exports field conditions handled, since exportsCategory and engines are mutually exclusive and neither applies unless you opt in.

Frequently asked questions

What does the resolve package do that require.resolve cannot?

resolve implements the Node require.resolve algorithm so you can resolve a module on behalf of another file, using the basedir option, and it works both asynchronously and synchronously. Node's own require.resolve resolves relative to the calling file and cannot be pointed at another directory the same way.

How do I use resolve synchronously?

Require the sync subpath, as in var resolve = require('resolve/sync'), or use the .sync property on the root export, require('resolve').sync, and call it with an id and an options object containing basedir to get the resolved path returned directly.

What do the resolve error codes mean?

MODULE_NOT_FOUND means the id could not be resolved, INVALID_BASEDIR means opts.basedir is missing or is not a directory, INVALID_PACKAGE_MAIN means a package.json has an invalid main property, ERR_PACKAGE_PATH_NOT_EXPORTED means the subpath is not in the exports field, ERR_INVALID_PACKAGE_CONFIG means the exports field has an invalid target, and INVALID_EXPORTS_CATEGORY means an invalid exportsCategory was passed.

How do exports field conditions work in resolve?

They are opt in. Setting exportsCategory to a node-exports-info category such as 'conditions' applies that category's semantics and also enables self-reference, while engines accepts a semver string or true to derive the category from the Node version. The two options are mutually exclusive, and moduleSystem and conditions only take effect when one of them is set.

Which version of resolve should I install?

The manifest in the repository is a prerelease at 2.0.0-next.7, and the repository publishes no GitHub releases. The last push was on 2026-10-06 and the package is licensed under MIT.

Official sources

  1. browserify/resolve on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/browserify-resolve.svg)](https://hysenlabs.com/projects/browserify-resolve)

Was this useful?