Launch Week Day 5: Introducing Reachability for PHP.Learn More
Socket
Book a DemoSign in
Socket

enhanced-resolve

Package Overview
Dependencies
Maintainers
8
Versions
139
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

enhanced-resolve

Offers a async require.resolve function. It's highly configurable.

latest
Source
npmnpm
Version
5.21.0
Version published
Weekly downloads
75M
-12.52%
Maintainers
8
Weekly downloads
 
Created
Source

enhanced-resolve

npm Build Status codecov Install Size GitHub Discussions

Offers an async require.resolve function. It's highly configurable.

Features

  • plugin system
  • provide a custom filesystem
  • sync and async node.js filesystems included

Getting Started

Install

# npm
npm install enhanced-resolve
# or Yarn
yarn add enhanced-resolve
# or pnpm
pnpm add enhanced-resolve

Resolve

There 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"
});

Public API

All of the following are exposed from require("enhanced-resolve").

resolve(context?, path, request, resolveContext?, callback)

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"
});

resolve.sync(context?, path, request, resolveContext?) => string | false

Synchronous variant. Throws on failure, returns false when the resolve yields no result.

const file = resolve.sync(__dirname, "./utils");

resolve.promise(context?, path, request, resolveContext?) => Promise<string | false>

Promise variant of resolve.

const file = await resolve.promise(__dirname, "./utils");

resolve.create(options) => ResolveFunctionAsync

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"
});

resolve.create.sync(options) => ResolveFunction

Sync variant of resolve.create.

const resolveTsSync = resolve.create.sync({ extensions: [".ts", ".js"] });
const file = resolveTsSync(__dirname, "./component");

resolve.create.promise(options) => ResolveFunctionPromise

Promise variant of resolve.create.

const resolveTsPromise = resolve.create.promise({ extensions: [".ts", ".js"] });
const file = await resolveTsPromise(__dirname, "./component");

ResolverFactory.createResolver(options) => Resolver

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", {});

CachedInputFileSystem(fileSystem, duration)

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");

Exported plugins & helpers

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 a log function on the resolveContext.
  • TsconfigPathsPlugin(options) — applies tsconfig.json paths / baseUrl mappings; typically configured via the tsconfig resolver 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) },
	() => {},
);

Creating a Resolver

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);

Resolver Options

FieldDefaultDescription
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
extensionAliasForExportsfalseAlso apply extensionAlias to paths resolved through the package.json exports field. Off by default (Node.js-aligned)
cachePredicatefunction() { 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.
cacheWithContexttrueIf 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
enforceExtensionfalseEnforce 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
fileSystemThe file system which should be used
fullySpecifiedfalseRequest 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
resolverundefinedA prepared Resolver to which the plugins are attached
resolveToContextfalseResolve to a context instead of a file
preferRelativefalsePrefer to resolve module requests as relative request and fallback to resolving as module
preferAbsolutefalsePrefer 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
symlinkstrueWhether to resolve symlinks to their symlinked location
tsconfigfalseTypeScript 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.configFiletsconfig.jsonPath to the tsconfig.json file
tsconfig.references[]Project references. 'auto' to load from tsconfig, or an array of paths to referenced projects
tsconfig.baseUrlundefinedOverride baseUrl from tsconfig.json. If provided, this value will be used instead of the baseUrl in the tsconfig file
unsafeCachefalseUse this cache object to unsafely cache the successful requests

Option Examples

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" })],
};

Plugins

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.

Built-in Plugins

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.

PluginPurpose
AliasPluginReplaces a matching request with one or more alternative targets. Powers the alias and fallback options.
AliasFieldPluginApplies aliasing based on a field in the description file (e.g. the browser field). Powers aliasFields.
AppendPluginAppends a string (typically an extension) to the current path. Used for extensions.
CloneBasenamePluginJoins the current directory basename onto the path (e.g. /foo/bar/foo/bar/bar). Useful for directory-named main-file schemes.
ConditionalPluginForwards the request only when it matches a given partial request shape.
DescriptionFilePluginFinds and loads the nearest description file (e.g. package.json) so other plugins can read its fields. Powers descriptionFiles.
DirectoryExistsPluginOnly continues the pipeline if the current path is an existing directory.
ExportsFieldPluginResolves requests through the exports field of a package's description file. Powers exportsFields and conditionNames.
ExtensionAliasPluginMaps one extension to a list of alternative extensions (e.g. .js.ts, .js). Powers extensionAlias.
FileExistsPluginOnly continues the pipeline if the current path is an existing file, and records the file as a dependency.
ImportsFieldPluginResolves #name requests through the imports field of the enclosing package.
JoinRequestPluginJoins the current path with the current request into a new path.
JoinRequestPartPluginSplits a module request into module name + inner request, joining the inner request onto the path.
LogInfoPluginEmits verbose log output at a given pipeline step. Handy for debugging resolves via resolveContext.log.
MainFieldPluginUses a field in the description file (e.g. main) to point to the entry file of a package. Powers mainFields.
ModulesInHierarchicalDirectoriesPluginSearches for a module by walking up parent directories (the standard node_modules lookup). Powers modules.
ModulesInRootPluginSearches for a module in a single absolute directory. Powers absolute-path entries in modules.
NextPluginForwards the request from one hook to another without modification — glue between pipeline steps.
ParsePluginParses a raw request string into its components (path, query, fragment, module flag, etc.).
PnpPluginResolves module requests through a Yarn PnP API when one is available.
RestrictionsPluginRejects results that don't match a list of path restrictions (strings or regular expressions). Powers restrictions.
ResultPluginTerminal plugin that fires the result hook — signals a successful resolve.
RootsPluginResolves server-relative URL requests (starting with /) against one or more root directories. Powers roots.
SelfReferencePluginResolves a package self-reference (e.g. my-pkg/foo from within my-pkg).
SymlinkPluginReal paths the resolved file by following symlinks. Can be disabled via the symlinks option.
TryNextPluginForwards the request to the next hook with a log message. Useful for trying alternative resolutions.
TsconfigPathsPluginRewrites requests using the paths and baseUrl from a tsconfig.json. Powers the tsconfig option.
UnsafeCachePluginCaches successful resolves in an in-memory map to speed up repeated requests. Powers unsafeCache.
UseFilePluginJoins a fixed filename onto the current path (e.g. index). Powers mainFiles.

Plugin wiring and goals

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-resolveinternal-resolve for alias; fileinternal-resolve as a last-chance remap; described-resolveinternal-resolve for fallback.
  • AliasFieldPlugin — Goal: apply aliases declared in a description-file field like browser, so environment-specific substitutions happen without user config. raw-resolve / fileinternal-resolve.
  • AppendPlugin — Goal: try appending a fixed string (usually an extension) to the current path. raw-filefile, one instance per entry in extensions.
  • CloneBasenamePlugin — Goal: join the directory's basename onto the path (e.g. /foo/bar/foo/bar/bar) for directory-named-main layouts. User-wired via plugins.
  • 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 on after-normal-resolve, resolve-as-module, described-relative, and raw-file.
  • DescriptionFilePlugin — Goal: locate and attach the nearest description file (usually package.json) so downstream plugins can read its fields. parsed-resolvedescribed-resolve, relativedescribed-relative, undescribed-resolve-in-packageresolve-in-package, undescribed-existing-directoryexisting-directory, undescribed-raw-fileraw-file.
  • DirectoryExistsPlugin — Goal: only continue the pipeline if the current path exists as a directory. resolve-as-moduleundescribed-resolve-in-package, directoryundescribed-existing-directory.
  • ExportsFieldPlugin — Goal: map a request through the exports field of a package's description file (with conditionNames). resolve-in-packagerelative.
  • ExtensionAliasPlugin — Goal: rewrite a request's extension to a list of candidate extensions (e.g. .js.ts, .js) for TypeScript ESM and similar. raw-resolvenormal-resolve for direct requests; also imports-field-relativerelative so extension substitution applies to imports-field targets.
  • FileExistsPlugin — Goal: confirm a candidate path exists as a file and record it as a file dependency. final-fileexisting-file.
  • ImportsFieldPlugin — Goal: resolve #name requests through the imports field of the enclosing package. internalimports-field-relative (relative target) or imports-resolve (bare target).
  • JoinRequestPlugin — Goal: join the current path with the current request into a single concrete path. after-normal-resolverelative (three stage-offset copies for preferRelative, preferAbsolute, and default), resolve-in-existing-directoryrelative.
  • JoinRequestPartPlugin — Goal: split a module request into module name + inner request, joining the inner part onto the path. moduleresolve-as-module.
  • LogInfoPlugin — Goal: emit verbose log output at a chosen hook; enable by passing a log function on resolveContext. User-wired via plugins.
  • MainFieldPlugin — Goal: follow a description-file field (e.g. main, module, browser) to the entry file of a package. existing-directoryresolve-in-existing-directory, one instance per entry in mainFields.
  • ModulesInHierarchicalDirectoriesPlugin — Goal: search for a module by walking up parent directories (the standard node_modules lookup). raw-modulemodule; when PnP is enabled, alternate-raw-modulemodule too.
  • ModulesInRootPlugin — Goal: search for a module in a single absolute directory (powers absolute-path entries in modules). raw-modulemodule.
  • 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 / internal flags. resolveparsed-resolve; also wired on internal-resolve and imports-resolve.
  • PnpPlugin — Goal: resolve bare-module requests through Yarn's PnP API when available. raw-moduleundescribed-resolve-in-package on hit, alternate-raw-module on miss.
  • RestrictionsPlugin — Goal: reject resolved paths that don't satisfy at least one string prefix or RegExp. Tapped on resolved.
  • ResultPlugin — Goal: terminal plugin — fires the result lifecycle hook and signals a successful resolve. Tapped on resolved.
  • RootsPlugin — Goal: resolve server-relative URL requests (starting with /) against one or more root directories. after-normal-resolverelative.
  • SelfReferencePlugin — Goal: resolve a package self-reference (my-pkg/foo from inside my-pkg) via its own exports. raw-moduleresolve-as-module.
  • SymlinkPlugin — Goal: real-path the resolved file by following symlinks; can be disabled via symlinks: false. existing-fileexisting-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-filefile (as the "no extension" attempt) and user-wired.
  • TsconfigPathsPlugin — Goal: rewrite requests using the paths and baseUrl from a tsconfig.json (including project references). Taps described-resolve internally and forwards to internal-resolve; exported for direct use as well.
  • UnsafeCachePlugin — Goal: cache successful resolves in an in-memory map for repeated requests. described-resolveraw-resolve (only when unsafeCache is enabled).
  • UseFilePlugin — Goal: join a fixed filename (e.g. index) onto the current path to try as an entry file. existing-directory / undescribed-existing-directoryundescribed-raw-file, one instance per entry in mainFiles.

Hooks

A resolver exposes two kinds of tapable hooks:

  • Lifecycle hooks on resolver.hooks — fired by the resolver itself around each resolve call. Use these to observe, not to transform the request.
  • Pipeline hooks — the named steps that plugins tap as source and forward to as target. Every pipeline hook is an AsyncSeriesBailHook<[request, resolveContext], request | null>: return callback() to pass on, callback(err) to fail, or callback(null, request) to short-circuit with a result. Obtain them with resolver.ensureHook(name) (creates if missing) or resolver.getHook(name) (throws if missing); names are kebab-case or camelCase and are interchangeable.

Lifecycle hooks

HookTypeFires when
resolveStepSyncHookEvery time the resolver hands a request to a pipeline hook. Arguments: (hook, request). Ideal for tracing.
noResolveSyncHookWhen a top-level resolve() call can't produce a result. Arguments: (request, error).
resolveAsyncSeriesBailHookEntry point of the pipeline (also listed below). Tap this to intercept requests before parsing.
resultAsyncSeriesHookAfter a successful resolve, with the final request. Fired by ResultPlugin. Tap to observe/record results without short-circuiting.

Pipeline hooks

Listed roughly in the order the default pipeline visits them. Full wiring lives in lib/ResolverFactory.js under //// pipeline ////.

HookRole
resolveEntry point. ParsePlugin parses the raw request (path, query, fragment, module flag) and forwards to parsed-resolve.
internal-resolveRe-entry point used by internal rewrites (e.g. after an alias fires). Same role as resolve, but fullySpecified is forced off.
imports-resolveRe-entry point for the target of an imports field match; prevents recursive # resolution per the Node.js ESM spec.
parsed-resolveRequest has been parsed. DescriptionFilePlugin attaches the nearest package.json, then forwards to described-resolve.
described-resolveDescription file is attached. Where unsafeCache, fallback, and most user plugins (including MyLibSrcPlugin below) hook in.
raw-resolveAfter description. Where alias, aliasFields, tsconfig paths, and extensionAlias rewrites fire before default resolution.
normal-resolveDefault 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-relativeConcrete 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-moduleBare-module lookup. SelfReferencePlugin, ModulesInHierarchicalDirectoriesPlugin, ModulesInRootPlugin, and PnpPlugin all tap here.
alternate-raw-moduleFallback module lookup used by PnpPlugin when PnP can't resolve and node_modules should be tried.
moduleA candidate module directory was built. JoinRequestPartPlugin splits off the inner request and forwards to resolve-as-module.
resolve-as-moduleTreat candidate as a package. DirectoryExistsPlugin gates on existence; short single-file modules may re-enter via undescribed-raw-file.
undescribed-resolve-in-packageInside a located package directory, before its package.json has been read. Loads the description, forwards to resolve-in-package.
resolve-in-packageInside a package with its description loaded. ExportsFieldPlugin matches exports, otherwise forwards to resolve-in-existing-directory.
resolve-in-existing-directoryPackage directory confirmed; join the remaining request onto it and continue at relative.
relativeA concrete path on disk. DescriptionFilePlugin loads the nearest package.json and forwards to described-relative.
described-relativeBranches to raw-file (treat as file) and directory (treat as directory). resolveToContext skips the file branch.
directoryCandidate directory. DirectoryExistsPlugin gates on existence and forwards to undescribed-existing-directory.
undescribed-existing-directoryExisting directory, before its package.json has been read. UseFilePlugin tries mainFiles via undescribed-raw-file.
existing-directoryExisting directory with description loaded. MainFieldPlugin tries mainFields; UseFilePlugin falls back to mainFiles.
undescribed-raw-fileCandidate file path, before description is read. Loads description, then forwards to raw-file.
raw-fileApply extension handling: ConditionalPlugin short-circuits when fullySpecified, TryNextPlugin + AppendPlugin try each extension.
fileA specific file path. Last place alias and aliasFields can redirect; forwards to final-file.
final-fileFileExistsPlugin checks the file is real and records it as a file dependency, then forwards to existing-file.
existing-fileReal file on disk. SymlinkPlugin real-paths symlinks (unless symlinks: false), then forwards to resolved.
resolvedTerminal hook. RestrictionsPlugin enforces restrictions; ResultPlugin fires the result lifecycle hook.

before- and after- prefixes

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.

Request flow by request type

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)

Hook examples

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"), …)).

Writing a Custom Plugin

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 same source hook. 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, or callback(err) to fail the resolve.
  • See Hooks for the full list of pipeline hooks, their order, and the before- / after- stage modifiers. lib/ResolverFactory.js has the exact wiring under //// pipeline ////.

Escaping

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.

Tests

npm run test

Passing options from webpack

If 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()]
  ...
},

License

Copyright (c) 2012-2019 JS Foundation and other contributors

MIT (http://www.opensource.org/licenses/mit-license.php)

FAQs

Package last updated on 23 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts