enhanced-resolve is require.resolve, asynchronous and configurable
Offers an async require.resolve function. It's highly configurable.
At a glance
- What is it?
- enhanced-resolve is the resolver library from the webpack family, offering an async require.resolve with sync, callback and promise forms, a plugin system, a pluggable filesystem and a collector that reports every path it probed. It is infrastructure that most teams meet through webpack rather than choose directly.
- Who is it for?
- enhanced-resolve fits a team that needs to resolve module specifiers the way a bundler does rather than the way the Node.js runtime does, in a build tool, a watcher, or a test runner, and it fits anyone who needs to know which paths a resolution touched. It is the wrong choice if all you need is Node's own require.resolve, since that ships with the runtime and understands far fewer options.
- 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 3 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
A resolver with the Node.js rules and a config surface
enhanced-resolve offers an async require.resolve function, and the second half of that description is just as important as the first: it is highly configurable.
The library resolves requests according to the Node.js resolving rules, which means the output matches what a runtime import would find, but the mechanism is different. Node's own resolver is synchronous and has no options. This one is asynchronous by design, because a bundler has to consider filesystems that are not local disk, and because a watch mode needs to know what it touched.
The feature list names three things: a plugin system, the ability to provide a custom filesystem, and sync and async Node.js filesystems included out of the box.
That is a deliberately small surface, and each item is load bearing. The custom filesystem is what makes the resolver usable over a virtual or remote filesystem. The plugin system is what lets you change behaviour, for instance to add an alias, change extension resolution, or refuse a module. The included filesystems mean you do not have to write one to get started.
The project is maintained under the webpack organisation, with Tobias Koppers listed as the author, and the topic list names commonjs, esm, javascript, resolve and resolver. The npm package is enhanced-resolve, currently at version 5.26.0, and the entry point is lib/index.js with type definitions in types.d.ts.
Four call shapes for the same resolver
The public API is a single function with an optional prefix of arguments. The documented signature is resolve(context?, parent, specifier, resolveContext?, callback), and both context and resolveContext are optional, which produces four shapes you can call.
resolve(context, parent, specifier, resolveContext, callback);
resolve(context, parent, specifier, callback);
resolve(parent, specifier, resolveContext, callback);
resolve(parent, specifier, callback); // most commonThe naming follows the ECMAScript resolution terms, the same ones used by import.meta.resolve, where parent is the location you resolve from and specifier is the string being resolved. The documentation is explicit that the older Node.js vocabulary called these the lookup start path and the request.
The parent argument is the absolute directory to resolve from, and it is always required. The specifier is the string, which may be a relative path, a bare module name, or an alias. The callback receives three values: an error, the resolved absolute path, and the full request object. A result of false is a real possibility, meaning the request resolved to an ignored module rather than to a file.
The context argument is extra information about who is asking, for example an object with an environments key, and plugins can read arbitrary keys off it. The guidance is that most callers omit it and that you only pass it when a plugin you use actually reads from it.
The resolver tells you which paths it probed
The resolveContext argument is the feature that separates this from a filename lookup. It is a collector object that the resolver writes into and reads from, and it holds fileDependencies, contextDependencies and missingDependencies as Sets that it fills in, plus a log function it calls for trace output.
const fileDependencies = new Set();
const missingDependencies = new Set();
const contextDependencies = new Set();
const logs = [];Passing that object to resolve fills the Sets as the resolver works, with fileDependencies holding files that were read or checked, missingDependencies holding paths that were probed but did not exist, and contextDependencies holding directories that were read. The log callback receives each resolution step, so the trace of a single resolve can be captured as an array of messages.
The documentation explains the intended use directly: fileDependencies and missingDependencies are exactly what you would feed a file watcher so a rebuild re-runs when any of them changes.
This is where the design earns its keep. A bundler that only returns a path cannot tell you that a resolution almost succeeded, that a package.json you are relying on has an exports field, or that the directory it walked is a context dependency. Collecting the misses turns resolution into an auditable operation, and the log callback gives you the step by step trace when a resolve behaves differently than you expect.
The cost is memory. Every candidate probed is retained for the lifetime of the Sets, which for a large project means the missing paths accumulate, and in a long running process that is worth watching.
Query strings and fragments survive the round trip
The third callback argument is the full request object, and it carries fields the resolved path alone does not. The documentation shows the pattern with a specifier that includes both a query and a fragment.
resolve(__dirname, "./utils?query#frag", (err, result, resolveRequest) => {
result; // === "/abs/path/to/utils.js?query#frag" — path with query/fragment appended
resolveRequest.path; // === "/abs/path/to/utils.js" (the bare path, no query/fragment)
resolveRequest.query; // === "?query" (empty string if the request has none)
resolveRequest.fragment; // === "#frag" (empty string if the request has none)
resolveRequest.descriptionFilePath; // path to the nearest package.json, if any
});The distinction between result and resolveRequest.path is the kind of detail that costs an afternoon if you do not know it. The result string keeps the query and the fragment appended, because that is what the importing module expects to see in its import specifier. The path field is the bare filesystem path.
The descriptionFilePath field is the location of the nearest package.json, if there is one. That is how a resolver-aware tool knows which manifest applied to the file it just found, which matters whenever exports or type fields influence resolution.
The query and fragment fields come back as empty strings when the request has none, rather than undefined, so code that appends them unconditionally does not have to guard.
create() gives you a resolver with your own rules
The defaults are fixed in the top level function. The documentation states that the async Node-style resolver uses the built-in defaults, which are conditionNames set to node, and extensions limited to .js, .json and .node.
That is a narrower set of rules than a modern bundler applies, and it is the reason create exists. The create method allows you to create a custom resolve function with its own options.
const myResolve = resolve.create({
// or resolve.create.sync / resolve.create.promise
extensions: [".ts", ".js"],
// see more options below
});
myResolve("/some/path/to/folder", "ts-module", (err, result) => {
result; // === "/some/node_modules/ts-module/index.ts"
});The example is small and the point is large. Once you have your own resolver you can also choose your own sync or promise flavour, since resolve.create.sync and resolve.create.promise exist as well as the callback form. The underlying API always offers all three, with sync, async callback and promise variants of the top level function too.
The plugin system is the other half of the same idea. Options and plugins together are what make the library highly configurable rather than merely configurable, because a plugin can change resolution steps rather than only supplying values.
One caveat about the documentation: the section on the options list is not visible in the text available here, so the exact set of option names beyond extensions, conditionNames and the file system hooks is something to confirm in the repository rather than assume. The comment in the example says to see more options below, and that list is what you should read before configuring anything unusual.
A browser build and a filesystem you can replace
The package.json contains a browser field that swaps five Node.js modules for browser specific replacements.
"process": "./lib/util/process-browser.js",
"module": "./lib/util/module-browser.js",
"path": "./lib/util/path-browser.js",
"url": "./lib/util/url-browser.js",
"graceful-fs": "./lib/util/graceful-fs-browser.js"Those are the modules a resolver touches by accident rather than by intent: process for the environment, module and path for module resolution semantics, url for specifier parsing, and graceful-fs as the filesystem layer. Mapping each to a browser variant is what allows the resolver to run in a browser bundle, which matters for tools that resolve module specifiers on the client side.
The custom filesystem feature is the more interesting half for build tooling. A filesystem in this sense is the thing the resolver reads through: paths in, contents or existence out. Replace it and the resolver will work against something that is not a disk, whether that is an in-memory archive, a virtual file system for a server-side build, or a remote store.
That capability is also the reason to read the license and the internals before reaching for it in a plugin, because the filesystem interface is the contract everything else depends on. The repository ships lib and types directories, with the type definitions generated and checked in the package, plus a benchmark directory and a test directory, so the public surface is meant to be inspected rather than reverse engineered.
A dependency of webpack rather than a project of its own
The practical reality is that most teams encounter enhanced-resolve through webpack, which uses it under the hood, and the decisions are then made for them. That is worth understanding before deciding to depend on it directly, because the version you get is the version webpack pins.
The release history shows steady work. Version 5.25.0 was published on 2026-09-12, 5.25.1 on 2026-09-13 and 5.26.0 on 2026-09-29, and the last push was on 2026-10-19 with the repository not archived. Patch releases land quickly after a minor one, which is the pattern of a package that is depended on by a large number of projects and is therefore careful about regressions.
The repository structure says something about the engineering effort. There is a .changeset directory, which is the tool used to prepare release notes, a CHANGELOG.md, an AGENTS.md and a CLAUDE.md for automated tooling conventions, husky for git hooks, prettier and eslint configuration, a jest config, a cspell dictionary, and separate tsconfig files for the project and for the generated type tests.
The licence is MIT, which is the permissive choice with no obligation on a service you deploy, and the author field names Tobias Koppers. That is a licensing summary and not legal advice.
The honest framing is that this is infrastructure. Its users are tools, and the decisions that matter are the ones a tool author makes about extensions, condition names, plugins and filesystems. If your team is not writing a resolver-aware tool, webpack already made these choices for you.
Editorial conclusion
enhanced-resolve fits a team that needs to resolve module specifiers the way a bundler does rather than the way the Node.js runtime does, in a build tool, a watcher, or a test runner, and it fits anyone who needs to know which paths a resolution touched. It is the wrong choice if all you need is Node's own require.resolve, since that ships with the runtime and understands far fewer options. Before adopting it, check the defaults the documentation names, conditionNames set to node and extensions limited to .js, .json and .node, because create() inherits them unless you override them, and note that the plugin and filesystem hooks are the parts most likely to need real testing in your environment.
Frequently asked questions
How do I install enhanced-resolve?
The documentation shows three package managers, npm install enhanced-resolve, yarn add enhanced-resolve or pnpm add enhanced-resolve. The published version is 5.26.0, the entry point is lib/index.js and type definitions ship in types.d.ts.
What is the difference between resolve, resolve.sync and resolve.promise?
All three resolve a specifier the way Node.js would, and the documentation offers sync, async callback and promise forms of the top level function. The top level resolve uses the built-in defaults, which are conditionNames set to node and extensions of .js, .json and .node.
How do I find out which files a resolution touched?
Pass a resolveContext object with fileDependencies, contextDependencies and missingDependencies Sets, plus an optional log function. The resolver fills the Sets as it works, and the documentation notes that the file and missing dependency sets are exactly what you would feed a file watcher.
Can I resolve TypeScript files with enhanced-resolve?
Yes, through create, which builds a custom resolve function with your own options. The documented example passes extensions of [".ts", ".js"] to resolve.create, and the resulting resolver returns the index.ts file of a package.
What does the third callback argument contain?
It is the full request object, with path, query, fragment and descriptionFilePath fields. The path is the bare filesystem path without the query or fragment, while the resolved result string keeps the query and fragment appended.
Can enhanced-resolve run in a browser?
The package.json defines a browser field that swaps process, module, path, url and graceful-fs for browser specific implementations under lib/util. The library also lets you provide a custom filesystem, which is what allows resolution against something other than the local disk.
Official sources
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.
[](https://hysenlabs.com/projects/webpack-enhanced-resolve)Was this useful?