Offers an async require.resolve function. It's highly configurable.
- plugin system
- provide a custom filesystem
- sync and async node.js filesystems included
# npm
npm install enhanced-resolve
# or Yarn
yarn add enhanced-resolve
# or pnpm
pnpm add enhanced-resolveThere is a Node.js API which allows to resolve requests according to the Node.js resolving rules.
Sync, async (callback) and promise APIs are offered. A create method allows to create a custom resolve function.
const resolve = require("enhanced-resolve");
resolve("/some/path/to/folder", "module/dir", (err, result) => {
result; // === "/some/path/node_modules/module/dir/index.js"
});
resolve.sync("/some/path/to/folder", "../../dir");
// === "/some/path/dir/index.js"
const result = await resolve.promise("/some/path/to/folder", "../../dir");
// === "/some/path/dir/index.js"
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"
});All of the following are exposed from require("enhanced-resolve").
Async Node-style resolver using the built-in defaults (conditionNames: ["node"], extensions: [".js", ".json", ".node"]). context is optional; when omitted, a built-in Node context is used.
const resolve = require("enhanced-resolve");
resolve(__dirname, "./utils", (err, result) => {
// result === "/abs/path/to/utils.js"
});Synchronous variant. Throws on failure, returns false when the resolve yields no result.
const file = resolve.sync(__dirname, "./utils");Promise variant of resolve.
const file = await resolve.promise(__dirname, "./utils");Builds a customized async resolve function. Options are the same as for ResolverFactory.createResolver; fileSystem defaults to the built-in Node.js filesystem.
const resolveTs = resolve.create({ extensions: [".ts", ".tsx", ".js"] });
resolveTs(__dirname, "./component", (err, result) => {
// result === "/abs/path/to/component.tsx"
});Sync variant of resolve.create.
const resolveTsSync = resolve.create.sync({ extensions: [".ts", ".js"] });
const file = resolveTsSync(__dirname, "./component");Promise variant of resolve.create.
const resolveTsPromise = resolve.create.promise({ extensions: [".ts", ".js"] });
const file = await resolveTsPromise(__dirname, "./component");Lower-level factory. Returns a Resolver whose resolve, resolveSync, and resolvePromise methods accept (context, path, request, resolveContext, [callback]). Use this when you need a reusable resolver instance or access to its hooks (see the Plugins section). fileSystem is required here — the high-level resolve.create defaults it for you.
const fs = require("fs");
const { CachedInputFileSystem, ResolverFactory } = require("enhanced-resolve");
const resolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".js", ".json"],
});
// callback
resolver.resolve({}, __dirname, "./utils", {}, (err, file) => {
// ...
});
// sync (requires a sync fileSystem)
const fileSync = resolver.resolveSync({}, __dirname, "./utils");
// promise
const filePromise = await resolver.resolvePromise({}, __dirname, "./utils", {});Wraps any Node-compatible fs to add an in-memory cache for stat, readdir, readFile, readJson, and readlink. duration is the cache TTL in milliseconds (typically 4000). Call .purge() to invalidate, or .purge(path) / .purge([path, ...]) to invalidate specific entries — do this whenever you know files changed (e.g. from a watcher).
const fs = require("fs");
const { CachedInputFileSystem } = require("enhanced-resolve");
const cachedFs = new CachedInputFileSystem(fs, 4000);
// later, when files change:
cachedFs.purge("/abs/path/to/changed-file.js");For use with the plugins option or as standalone utilities:
ResolverFactory— see above.CachedInputFileSystem— see above.CloneBasenamePlugin(source, target)— joins the directory's basename onto the path. See Built-in Plugins.LogInfoPlugin(source)— logs pipeline state at a hook; enable by passing alogfunction on theresolveContext.TsconfigPathsPlugin(options)— appliestsconfig.jsonpaths/baseUrlmappings; typically configured via thetsconfigresolver option instead.forEachBail(array, iterator, callback)— bail-style async iterator used internally; useful when authoring plugins that try several candidates in order.
const { LogInfoPlugin } = require("enhanced-resolve");
const resolver = ResolverFactory.createResolver({
fileSystem: cachedFs,
extensions: [".js"],
plugins: [new LogInfoPlugin("described-resolve")],
});
resolver.resolve(
{},
__dirname,
"./utils",
{ log: (msg) => console.log(msg) },
() => {},
);The easiest way to create a resolver is to use the createResolver function on ResolveFactory, along with one of the supplied File System implementations.
const fs = require("fs");
const { CachedInputFileSystem, ResolverFactory } = require("enhanced-resolve");
// create a resolver
const myResolver = ResolverFactory.createResolver({
// Typical usage will consume the `fs` + `CachedInputFileSystem`, which wraps Node.js `fs` to add caching.
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".js", ".json"],
/* any other resolver options here. Options/defaults can be seen below */
});
// resolve a file with the new resolver
const context = {};
const lookupStartPath = "/Users/webpack/some/root/dir";
const request = "./path/to-look-up.js";
const resolveContext = {};
// callback
myResolver.resolve(
context,
lookupStartPath,
request,
resolveContext,
(err /* Error */, filepath /* string */) => {
// Do something with the path
},
);
// promise
try {
const filepath = await myResolver.resolvePromise(
context,
lookupStartPath,
request,
resolveContext,
);
// Do something with the path
} catch (err) {
// handle resolve failure
}
// sync (requires a sync fileSystem, e.g. the default Node.js one)
const filepath = myResolver.resolveSync(context, lookupStartPath, request);| Field | Default | Description |
|---|---|---|
| alias | [] | A list of module alias configurations or an object which maps key to value |
| aliasFields | [] | A list of alias fields in description files |
| extensionAlias | {} | An object which maps extension to extension aliases |
| extensionAliasForExports | false | Also apply extensionAlias to paths resolved through the package.json exports field. Off by default (Node.js-aligned) |
| cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with path and request properties. |
| cacheWithContext | true | If unsafe cache is enabled, includes request.context in the cache key |
| conditionNames | [] | A list of exports field condition names |
| descriptionFiles | ["package.json"] | A list of description files to read from |
| enforceExtension | false | Enforce that a extension from extensions must be used |
| exportsFields | ["exports"] | A list of exports fields in description files |
| extensions | [".js", ".json", ".node"] | A list of extensions which should be tried for files |
| fallback | [] | Same as alias, but only used if default resolving fails |
| fileSystem | The file system which should be used | |
| fullySpecified | false | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) |
| mainFields | ["main"] | A list of main fields in description files |
| mainFiles | ["index"] | A list of main files in directories |
| modules | ["node_modules"] | A list of directories to resolve modules from, can be absolute path or folder name |
| plugins | [] | A list of additional resolve plugins which should be applied |
| resolver | undefined | A prepared Resolver to which the plugins are attached |
| resolveToContext | false | Resolve to a context instead of a file |
| preferRelative | false | Prefer to resolve module requests as relative request and fallback to resolving as module |
| preferAbsolute | false | Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots |
| restrictions | [] | A list of resolve restrictions |
| roots | [] | A list of root paths |
| symlinks | true | Whether to resolve symlinks to their symlinked location |
| tsconfig | false | TypeScript config for paths mapping. Can be false (disabled), true (use default tsconfig.json), a string path to tsconfig.json, or an object with configFile, references, and baseUrl options. Supports JSONC format (comments and trailing commas) like TypeScript compiler. |
| tsconfig.configFile | tsconfig.json | Path to the tsconfig.json file |
| tsconfig.references | [] | Project references. 'auto' to load from tsconfig, or an array of paths to referenced projects |
| tsconfig.baseUrl | undefined | Override baseUrl from tsconfig.json. If provided, this value will be used instead of the baseUrl in the tsconfig file |
| unsafeCache | false | Use this cache object to unsafely cache the successful requests |
Small snippets for the non-obvious options. All options are passed to resolve.create({ ... }) or ResolverFactory.createResolver({ ... }).
alias — rewrite matching requests to a target path, module, or to false to ignore them. Accepts an object or an array of entries (array form lets you specify ordering / onlyModule).
const options = {
alias: {
"@": path.resolve(__dirname, "src"), // @/utils → src/utils
lodash$: "lodash-es", // exact "lodash", not "lodash/foo"
"ignored-module": false, // short-circuit to an empty module
},
};aliasFields — read alias maps from fields in package.json. The browser field is the common case:
const options = { aliasFields: ["browser"] };extensionAlias — maps one request extension to a list of candidate extensions. Useful for TypeScript ESM where imports are written with .js but the source is .ts. Applies both to direct requests (e.g. ./foo.js) and to paths produced by the package.json imports field (e.g. #foo → ./foo.js → ./foo.ts). By default it does not apply to paths produced by the exports field (to stay aligned with Node.js, which does not substitute extensions on package-exported paths) — see extensionAliasForExports below to opt in:
const options = {
extensionAlias: {
".js": [".ts", ".js"],
".mjs": [".mts", ".mjs"],
},
};extensionAliasForExports — when true, also apply extensionAlias to paths resolved through the package.json exports field. Off by default to match Node.js. Turn it on if you want full alignment with TypeScript's resolver for packages that ship .ts sources alongside the compiled .js files they list in exports (e.g. monorepo source packages, or the eslint-import-resolver-typescript use case):
const options = {
extensionAlias: { ".js": [".ts", ".js"] },
extensionAliasForExports: true,
};conditionNames + exportsFields — pick which conditions to match in the exports field of package.json:
const options = {
conditionNames: ["import", "node", "default"],
exportsFields: ["exports"],
};extensions — extensions to try for extensionless requests, in order:
const options = { extensions: [".ts", ".tsx", ".js", ".json"] };fallback — same shape as alias, but only consulted when the primary resolve fails. Handy for polyfills:
const options = {
fallback: {
crypto: require.resolve("crypto-browserify"),
stream: false,
},
};modules — where to look for bare-module requests. Entries can be folder names (searched hierarchically up the tree) or absolute paths (searched directly):
const options = { modules: [path.resolve(__dirname, "src"), "node_modules"] };mainFields / mainFiles — fields in package.json to try for a package entry point, and filenames to try inside a directory:
const options = {
mainFields: ["browser", "module", "main"],
mainFiles: ["index"],
};roots + preferAbsolute — resolve server-relative URLs (starting with /) against one or more root directories. With preferAbsolute: true, absolute-path resolution is tried before the roots are consulted.
const options = {
roots: [path.resolve(__dirname, "public")],
preferAbsolute: false,
};restrictions — reject results that don't satisfy at least one restriction. Accepts strings (path prefixes) or RegExps:
const options = {
restrictions: [path.resolve(__dirname, "src"), /\.(js|ts)$/],
};tsconfig — apply TypeScript paths / baseUrl mappings. Either pass true to load ./tsconfig.json, a path string, or a configuration object:
const options = {
tsconfig: {
configFile: path.resolve(__dirname, "tsconfig.json"),
references: "auto", // honor project references declared in tsconfig
},
};symlinks — resolve to the real path by following symlinks. Set to false to keep the symlinked path (common for monorepo / pnpm layouts where you want module identity tied to the workspace location):
const options = { symlinks: false };fullySpecified — require fully-specified requests (no extension inference, no index lookup) for non-internal requests. Matches Node.js ESM semantics:
const options = { fullySpecified: true };unsafeCache — pass an object to use as an in-memory cache of successful resolves. Set to true to let the resolver allocate its own:
const options = {
unsafeCache: {}, // or true
cacheWithContext: false, // skip context in the cache key — faster, but only safe if context doesn't change the result
};To observe whether a request was served from the cache, wrap the cache object in a Proxy. UnsafeCachePlugin reads entries with cache[id] (cache lookup) and writes them with cache[id] = result (cache store), so trapping get and set is enough to distinguish hits from misses:
const cache = {};
const observedCache = new Proxy(cache, {
get(target, name, receiver) {
const hit = name in target;
console.log(hit ? `[cache hit] ${name}` : `[cache miss] ${name}`);
return Reflect.get(target, name, receiver);
},
set(target, name, value, receiver) {
console.log(`[cache set] ${name}`);
return Reflect.set(target, name, value, receiver);
},
});
const resolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".js", ".json"],
unsafeCache: observedCache,
});The name argument is the cache id — a JSON.stringify'd object containing type, context, path, query, fragment, and request — so you can parse it to report on specific resolves. Only successful resolves go through the cache; failures never touch it.
fileSystem — any fs-compatible implementation. Usually new CachedInputFileSystem(fs, 4000); can be a virtual filesystem (e.g. memfs) for testing:
const options = { fileSystem: new CachedInputFileSystem(require("fs"), 4000) };plugins — additional plugin instances appended to the pipeline. See Plugins:
const options = {
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
};Similar to webpack, the core of enhanced-resolve functionality is implemented as individual plugins that are executed using tapable.
These plugins can extend the functionality of the library, adding other ways for files/contexts to be resolved.
A plugin should be a class (or its ES5 equivalent) with an apply method. The apply method will receive a resolver instance, that can be used to hook in to the event system.
Plugins are executed in a pipeline, and register which event they should be executed before/after. source is the name of the event that starts the pipeline, and target is what event this plugin should fire, which is what continues the execution of the pipeline. For a full view of how these plugin events form a chain, see lib/ResolverFactory.js, in the //// pipeline //// section.
enhanced-resolve ships with the following plugins. Most of them are wired up automatically by ResolverFactory based on the resolver options; the ones exported from the package entry (TsconfigPathsPlugin, CloneBasenamePlugin, LogInfoPlugin) are the ones you're most likely to use explicitly.
| Plugin | Purpose |
|---|---|
AliasPlugin |
Replaces a matching request with one or more alternative targets. Powers the alias and fallback options. |
AliasFieldPlugin |
Applies aliasing based on a field in the description file (e.g. the browser field). Powers aliasFields. |
AppendPlugin |
Appends a string (typically an extension) to the current path. Used for extensions. |
CloneBasenamePlugin |
Joins the current directory basename onto the path (e.g. /foo/bar → /foo/bar/bar). Useful for directory-named main-file schemes. |
ConditionalPlugin |
Forwards the request only when it matches a given partial request shape. |
DescriptionFilePlugin |
Finds and loads the nearest description file (e.g. package.json) so other plugins can read its fields. Powers descriptionFiles. |
DirectoryExistsPlugin |
Only continues the pipeline if the current path is an existing directory. |
ExportsFieldPlugin |
Resolves requests through the exports field of a package's description file. Powers exportsFields and conditionNames. |
ExtensionAliasPlugin |
Maps one extension to a list of alternative extensions (e.g. .js → .ts, .js). Powers extensionAlias. |
FileExistsPlugin |
Only continues the pipeline if the current path is an existing file, and records the file as a dependency. |
ImportsFieldPlugin |
Resolves #name requests through the imports field of the enclosing package. |
JoinRequestPlugin |
Joins the current path with the current request into a new path. |
JoinRequestPartPlugin |
Splits a module request into module name + inner request, joining the inner request onto the path. |
LogInfoPlugin |
Emits verbose log output at a given pipeline step. Handy for debugging resolves via resolveContext.log. |
MainFieldPlugin |
Uses a field in the description file (e.g. main) to point to the entry file of a package. Powers mainFields. |
ModulesInHierarchicalDirectoriesPlugin |
Searches for a module by walking up parent directories (the standard node_modules lookup). Powers modules. |
ModulesInRootPlugin |
Searches for a module in a single absolute directory. Powers absolute-path entries in modules. |
NextPlugin |
Forwards the request from one hook to another without modification — glue between pipeline steps. |
ParsePlugin |
Parses a raw request string into its components (path, query, fragment, module flag, etc.). |
PnpPlugin |
Resolves module requests through a Yarn PnP API when one is available. |
RestrictionsPlugin |
Rejects results that don't match a list of path restrictions (strings or regular expressions). Powers restrictions. |
ResultPlugin |
Terminal plugin that fires the result hook — signals a successful resolve. |
RootsPlugin |
Resolves server-relative URL requests (starting with /) against one or more root directories. Powers roots. |
SelfReferencePlugin |
Resolves a package self-reference (e.g. my-pkg/foo from within my-pkg). |
SymlinkPlugin |
Real paths the resolved file by following symlinks. Can be disabled via the symlinks option. |
TryNextPlugin |
Forwards the request to the next hook with a log message. Useful for trying alternative resolutions. |
TsconfigPathsPlugin |
Rewrites requests using the paths and baseUrl from a tsconfig.json. Powers the tsconfig option. |
UnsafeCachePlugin |
Caches successful resolves in an in-memory map to speed up repeated requests. Powers unsafeCache. |
UseFilePlugin |
Joins a fixed filename onto the current path (e.g. index). Powers mainFiles. |
One-line goal and default wiring (source → target) for each plugin. * means the plugin is tapped on several hooks — the common ones are listed. Plugins without a fixed wiring are user-tapped.
AliasPlugin— Goal: redirect requests matching a configured key to an alternative target.raw-resolve→internal-resolveforalias;file→internal-resolveas a last-chance remap;described-resolve→internal-resolveforfallback.AliasFieldPlugin— Goal: apply aliases declared in a description-file field likebrowser, so environment-specific substitutions happen without user config.raw-resolve/file→internal-resolve.AppendPlugin— Goal: try appending a fixed string (usually an extension) to the current path.raw-file→file, one instance per entry inextensions.CloneBasenamePlugin— Goal: join the directory's basename onto the path (e.g./foo/bar→/foo/bar/bar) for directory-named-main layouts. User-wired viaplugins.ConditionalPlugin— Goal: gate a forward on a partial match of the request shape (e.g.{ module: true }), used to fan-out at branching hooks. Tapped onafter-normal-resolve,resolve-as-module,described-relative, andraw-file.DescriptionFilePlugin— Goal: locate and attach the nearest description file (usuallypackage.json) so downstream plugins can read its fields.parsed-resolve→described-resolve,relative→described-relative,undescribed-resolve-in-package→resolve-in-package,undescribed-existing-directory→existing-directory,undescribed-raw-file→raw-file.DirectoryExistsPlugin— Goal: only continue the pipeline if the current path exists as a directory.resolve-as-module→undescribed-resolve-in-package,directory→undescribed-existing-directory.ExportsFieldPlugin— Goal: map a request through theexportsfield of a package's description file (withconditionNames).resolve-in-package→relative.ExtensionAliasPlugin— Goal: rewrite a request's extension to a list of candidate extensions (e.g..js→.ts,.js) for TypeScript ESM and similar.raw-resolve→normal-resolvefor direct requests; alsoimports-field-relative→relativeso extension substitution applies toimports-field targets.FileExistsPlugin— Goal: confirm a candidate path exists as a file and record it as a file dependency.final-file→existing-file.ImportsFieldPlugin— Goal: resolve#namerequests through theimportsfield of the enclosing package.internal→imports-field-relative(relative target) orimports-resolve(bare target).JoinRequestPlugin— Goal: join the current path with the current request into a single concrete path.after-normal-resolve→relative(three stage-offset copies forpreferRelative,preferAbsolute, and default),resolve-in-existing-directory→relative.JoinRequestPartPlugin— Goal: split a module request into module name + inner request, joining the inner part onto the path.module→resolve-as-module.LogInfoPlugin— Goal: emit verbose log output at a chosen hook; enable by passing alogfunction onresolveContext. User-wired viaplugins.MainFieldPlugin— Goal: follow a description-file field (e.g.main,module,browser) to the entry file of a package.existing-directory→resolve-in-existing-directory, one instance per entry inmainFields.ModulesInHierarchicalDirectoriesPlugin— Goal: search for a module by walking up parent directories (the standardnode_moduleslookup).raw-module→module; when PnP is enabled,alternate-raw-module→moduletoo.ModulesInRootPlugin— Goal: search for a module in a single absolute directory (powers absolute-path entries inmodules).raw-module→module.NextPlugin— Goal: glue — forward the current request unchanged from one hook to another. Used across the pipeline wherever two hooks should run sequentially.ParsePlugin— Goal: split the raw request string into path / query / fragment /module/directory/internalflags.resolve→parsed-resolve; also wired oninternal-resolveandimports-resolve.PnpPlugin— Goal: resolve bare-module requests through Yarn's PnP API when available.raw-module→undescribed-resolve-in-packageon hit,alternate-raw-moduleon miss.RestrictionsPlugin— Goal: reject resolved paths that don't satisfy at least one string prefix or RegExp. Tapped onresolved.ResultPlugin— Goal: terminal plugin — fires theresultlifecycle hook and signals a successful resolve. Tapped onresolved.RootsPlugin— Goal: resolve server-relative URL requests (starting with/) against one or more root directories.after-normal-resolve→relative.SelfReferencePlugin— Goal: resolve a package self-reference (my-pkg/foofrom insidemy-pkg) via its ownexports.raw-module→resolve-as-module.SymlinkPlugin— Goal: real-path the resolved file by following symlinks; can be disabled viasymlinks: false.existing-file→existing-file(runs via a stage offset on the same hook).TryNextPlugin— Goal: forward the request to another hook with a log message, useful for trying an alternative candidate.raw-file→file(as the "no extension" attempt) and user-wired.TsconfigPathsPlugin— Goal: rewrite requests using thepathsandbaseUrlfrom atsconfig.json(including project references). Tapsdescribed-resolveinternally and forwards tointernal-resolve; exported for direct use as well.UnsafeCachePlugin— Goal: cache successful resolves in an in-memory map for repeated requests.described-resolve→raw-resolve(only whenunsafeCacheis enabled).UseFilePlugin— Goal: join a fixed filename (e.g.index) onto the current path to try as an entry file.existing-directory/undescribed-existing-directory→undescribed-raw-file, one instance per entry inmainFiles.
A resolver exposes two kinds of tapable hooks:
- Lifecycle hooks on
resolver.hooks— fired by the resolver itself around eachresolvecall. Use these to observe, not to transform the request. - Pipeline hooks — the named steps that plugins tap as
sourceand forward to astarget. Every pipeline hook is anAsyncSeriesBailHook<[request, resolveContext], request | null>: returncallback()to pass on,callback(err)to fail, orcallback(null, request)to short-circuit with a result. Obtain them withresolver.ensureHook(name)(creates if missing) orresolver.getHook(name)(throws if missing); names are kebab-case or camelCase and are interchangeable.
| Hook | Type | Fires when |
|---|---|---|
resolveStep |
SyncHook |
Every time the resolver hands a request to a pipeline hook. Arguments: (hook, request). Ideal for tracing. |
noResolve |
SyncHook |
When a top-level resolve() call can't produce a result. Arguments: (request, error). |
resolve |
AsyncSeriesBailHook |
Entry point of the pipeline (also listed below). Tap this to intercept requests before parsing. |
result |
AsyncSeriesHook |
After a successful resolve, with the final request. Fired by ResultPlugin. Tap to observe/record results without short-circuiting. |
Listed roughly in the order the default pipeline visits them. Full wiring lives in lib/ResolverFactory.js under //// pipeline ////.
| Hook | Role |
|---|---|
resolve |
Entry point. ParsePlugin parses the raw request (path, query, fragment, module flag) and forwards to parsed-resolve. |
internal-resolve |
Re-entry point used by internal rewrites (e.g. after an alias fires). Same role as resolve, but fullySpecified is forced off. |
imports-resolve |
Re-entry point for the target of an imports field match; prevents recursive # resolution per the Node.js ESM spec. |
parsed-resolve |
Request has been parsed. DescriptionFilePlugin attaches the nearest package.json, then forwards to described-resolve. |
described-resolve |
Description file is attached. Where unsafeCache, fallback, and most user plugins (including MyLibSrcPlugin below) hook in. |
raw-resolve |
After description. Where alias, aliasFields, tsconfig paths, and extensionAlias rewrites fire before default resolution. |
normal-resolve |
Default resolution starts. Branches into relative (for ./, ../, absolute), raw-module (bare modules), or internal (#imports). |
internal |
#name imports-field entry. ImportsFieldPlugin maps the specifier and forwards to imports-field-relative or imports-resolve. |
imports-field-relative |
Concrete path from an imports-field match, before the normal relative flow. ExtensionAliasPlugin taps here so .js → .ts also fires for #name targets. Forwards to relative. |
raw-module |
Bare-module lookup. SelfReferencePlugin, ModulesInHierarchicalDirectoriesPlugin, ModulesInRootPlugin, and PnpPlugin all tap here. |
alternate-raw-module |
Fallback module lookup used by PnpPlugin when PnP can't resolve and node_modules should be tried. |
module |
A candidate module directory was built. JoinRequestPartPlugin splits off the inner request and forwards to resolve-as-module. |
resolve-as-module |
Treat candidate as a package. DirectoryExistsPlugin gates on existence; short single-file modules may re-enter via undescribed-raw-file. |
undescribed-resolve-in-package |
Inside a located package directory, before its package.json has been read. Loads the description, forwards to resolve-in-package. |
resolve-in-package |
Inside a package with its description loaded. ExportsFieldPlugin matches exports, otherwise forwards to resolve-in-existing-directory. |
resolve-in-existing-directory |
Package directory confirmed; join the remaining request onto it and continue at relative. |
relative |
A concrete path on disk. DescriptionFilePlugin loads the nearest package.json and forwards to described-relative. |
described-relative |
Branches to raw-file (treat as file) and directory (treat as directory). resolveToContext skips the file branch. |
directory |
Candidate directory. DirectoryExistsPlugin gates on existence and forwards to undescribed-existing-directory. |
undescribed-existing-directory |
Existing directory, before its package.json has been read. UseFilePlugin tries mainFiles via undescribed-raw-file. |
existing-directory |
Existing directory with description loaded. MainFieldPlugin tries mainFields; UseFilePlugin falls back to mainFiles. |
undescribed-raw-file |
Candidate file path, before description is read. Loads description, then forwards to raw-file. |
raw-file |
Apply extension handling: ConditionalPlugin short-circuits when fullySpecified, TryNextPlugin + AppendPlugin try each extension. |
file |
A specific file path. Last place alias and aliasFields can redirect; forwards to final-file. |
final-file |
FileExistsPlugin checks the file is real and records it as a file dependency, then forwards to existing-file. |
existing-file |
Real file on disk. SymlinkPlugin real-paths symlinks (unless symlinks: false), then forwards to resolved. |
resolved |
Terminal hook. RestrictionsPlugin enforces restrictions; ResultPlugin fires the result lifecycle hook. |
ensureHook("before-foo") and getHook("before-foo") return the foo hook with stage: -10; after-foo returns it with stage: 10. Use this to tap earlier or later than the default stage without creating a separate hook. You'll see after-parsed-resolve, after-normal-resolve, after-relative, and after-undescribed-resolve-in-package used this way inside ResolverFactory.
The same 26 pipeline hooks serve every request, but different request shapes take different paths through them. Each ➝ below is one doResolve / NextPlugin / plugin forward; resolveStep fires on every arrow, so tapping it (see Hook examples) prints these chains live.
Relative path (./utils from /src/index.js) — the default "resolve on disk" path:
resolve (ParsePlugin)
➝ parsed-resolve (DescriptionFilePlugin attaches nearest package.json)
➝ described-resolve (NextPlugin; or UnsafeCachePlugin short-circuit)
➝ raw-resolve (NextPlugin; alias/tsconfig would branch here)
➝ normal-resolve (JoinRequestPlugin: path=/src/utils, request="")
➝ relative (DescriptionFilePlugin loads /src/package.json)
➝ described-relative (branches to file and directory candidates)
├─ ➝ raw-file (ConditionalPlugin / TryNextPlugin)
│ ➝ file (AppendPlugin tried each extension, e.g. .js)
│ ➝ final-file (FileExistsPlugin confirms the file)
│ ➝ existing-file (SymlinkPlugin real-paths it)
│ ➝ resolved (RestrictionsPlugin → ResultPlugin)
└─ ➝ directory (DirectoryExistsPlugin; used when path is a dir)
➝ undescribed-existing-directory
➝ existing-directory (MainFieldPlugin tries "main", UseFilePlugin tries "index")
➝ undescribed-raw-file ➝ raw-file ➝ …
Bare module (lodash/merge) — walks up node_modules, then treats the hit as a package:
resolve ➝ parsed-resolve ➝ described-resolve ➝ raw-resolve ➝ normal-resolve
➝ raw-module (ConditionalPlugin {module:true})
➝ module (ModulesInHierarchicalDirectoriesPlugin walks
/src/node_modules, /node_modules, …)
➝ resolve-as-module (JoinRequestPartPlugin splits "lodash" / "./merge")
➝ undescribed-resolve-in-package (DirectoryExistsPlugin gates on lodash/ existing)
➝ resolve-in-package (DescriptionFilePlugin loads lodash/package.json)
├─ ➝ relative (ExportsFieldPlugin, if "exports" matches)
└─ ➝ resolve-in-existing-directory (otherwise; JoinRequestPlugin joins "./merge")
➝ relative ➝ … (same tail as the relative flow above)
Internal import (#util from inside a package) — re-enters the pipeline after mapping:
resolve ➝ parsed-resolve ➝ described-resolve ➝ raw-resolve ➝ normal-resolve
➝ internal (ConditionalPlugin {internal:true})
➝ imports-resolve (ImportsFieldPlugin mapped "#util" to a bare target)
➝ parsed-resolve ➝ … (fresh pipeline run, internal:false so # isn't remapped)
When the imports field maps to a relative target, the branch instead goes:
➝ internal
➝ imports-field-relative (ImportsFieldPlugin mapped "#util" to "./util.js";
ExtensionAliasPlugin can swap .js → .ts here)
➝ relative ➝ … (same tail as the relative flow above)
Alias hit (@/button with alias: { "@": "/src" }) — rewrites then restarts:
resolve ➝ parsed-resolve ➝ described-resolve
➝ raw-resolve
➝ internal-resolve (AliasPlugin rewrote request → "/src/button")
➝ parsed-resolve ➝ … (fullySpecified forced off; AliasPlugin won't re-fire for the rewritten form)
exports-field hit inside a package (pkg/feature matching "./feature" in exports):
… ➝ raw-module ➝ module ➝ resolve-as-module ➝ undescribed-resolve-in-package
➝ resolve-in-package
➝ relative (ExportsFieldPlugin jumped to the exports target;
main-field / main-file logic is skipped)
➝ described-relative ➝ raw-file ➝ file ➝ final-file ➝ existing-file ➝ resolved
Failure — every candidate opts out (callback()) and no handler ever short-circuits with a result. noResolve fires once, for the top-level request:
… ➝ final-file
✗ FileExistsPlugin: ENOENT (opts out; no extension candidates left)
⇠ bail hooks unwind, each tapped handler has already tried its alternatives
⇒ top-level resolve() returns no result
⇒ resolver.hooks.noResolve(request, error) (ResultPlugin never fires)
Trace every pipeline step and observe failures via the lifecycle hooks:
resolver.hooks.resolveStep.tap("Trace", (hook, request) => {
console.log(`[step] ${hook.name}: ${request.request} @ ${request.path}`);
});
resolver.hooks.noResolve.tap("Trace", (request, error) => {
console.log(`[fail] ${request.request}: ${error.message}`);
});
resolver.hooks.result.tapAsync("Trace", (request, _ctx, callback) => {
console.log(`[done] ${request.path}`);
callback();
});Short-circuit at file to redirect any .css request to a stub without continuing the pipeline:
class StubCssPlugin {
apply(resolver) {
resolver
.getHook("file")
.tapAsync("StubCssPlugin", (request, _ctx, callback) => {
if (!request.path || !request.path.endsWith(".css")) return callback();
callback(null, { ...request, path: require.resolve("./empty.css") });
});
}
}Forward to a different hook with doResolve to restart resolution with a rewritten request — see MyLibSrcPlugin in Writing a Custom Plugin for the canonical pattern (getHook("described-resolve") → doResolve(ensureHook("resolve"), …)).
The example below adds a plugin that rewrites any request starting with my-lib/ to my-lib/src/. It taps the described-resolve hook (after the description file has been located) and forwards the rewritten request to resolve, so the pipeline restarts with the new request.
const fs = require("fs");
const { CachedInputFileSystem, ResolverFactory } = require("enhanced-resolve");
class MyLibSrcPlugin {
apply(resolver) {
const target = resolver.ensureHook("resolve");
resolver
.getHook("described-resolve")
.tapAsync("MyLibSrcPlugin", (request, resolveContext, callback) => {
if (!request.request || !request.request.startsWith("my-lib/")) {
return callback();
}
const newRequest = {
...request,
request: request.request.replace(/^my-lib\//, "my-lib/src/"),
};
resolver.doResolve(
target,
newRequest,
"rewrote my-lib → my-lib/src",
resolveContext,
callback,
);
});
}
}
const myResolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".js", ".json"],
plugins: [new MyLibSrcPlugin()],
});Tips for writing your own plugin:
- Call
callback()with no arguments to pass the request on to the next tapped handler at the samesourcehook. This is how you "opt out" when a request doesn't apply. - Call
resolver.doResolve(target, newRequest, message, resolveContext, callback)to continue the pipeline at a different hook with a (possibly modified) request. - Return early with
callback(null, result)to short-circuit with a specific result, orcallback(err)to fail the resolve. - See Hooks for the full list of pipeline hooks, their order, and the
before-/after-stage modifiers.lib/ResolverFactory.jshas the exact wiring under//// pipeline ////.
It's allowed to escape # as \0# to avoid parsing it as fragment.
enhanced-resolve will try to resolve requests containing # as path and as fragment, so it will automatically figure out if ./some#thing means .../some.js#thing or .../some#thing.js. When a # is resolved as path it will be escaped in the result. Here: .../some\0#thing.js.
npm run testIf you are using webpack, and you want to pass custom options to enhanced-resolve, the options are passed from the resolve key of your webpack configuration e.g.:
resolve: {
extensions: ['.js', '.jsx'],
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
plugins: [new DirectoryNamedWebpackPlugin()]
...
},
Copyright (c) 2012-2019 JS Foundation and other contributors