Resolver spec
May 25, 2025 · View on GitHub
To the extent it is feasible, trailing versions of the resolvers will continue to be supported, at least until a major version bump on the plugin proper.
Currently, version 1 is assumed if no interfaceVersion is available. (didn't think to define it until v2, heh. 😅)
v3
Resolvers must export the following (with name being optional):
Required interfaceVersion: number
The following document currently describes version 3 of the resolver interface. As such, a resolver implementing this version should
export const interfaceVersion = 3
or
exports.interfaceVersion = 3
Required resolve
Signature: (source: string, file: string) => { found: boolean, path?: string | null }
Given:
// /some/path/to/module.js
import ... from './imported-file'
and
// eslint.config.js
import { useRuleContext, getTsconfigWithContext } from 'eslint-import-context'
import { createNodeResolver } from 'eslint-plugin-import-x'
export default [
{
settings: {
'import/resolver-next': [
{
name: 'my-cool-resolver',
interfaceVersion: 3,
resolve(source, file) {
const ruleContext = useRuleContext()
const tsconfig = getTsconfigWithContext(ruleContext)
// use a factory to get config outside of the resolver
},
},
createNodeResolver({
modules: [a, b, c],
}),
],
},
},
]
useRuleContext and getTsconfigWithContext
They are powered by eslint-import-context in the above example, but they are not required to be used, and please be aware that useRuleContext() could be undefined when using eslint-plugin-import or old versions of eslint-plugin-import-x.
Arguments
The arguments provided will be:
source
the module identifier (./imported-file).
file
the absolute path to the file making the import (/some/path/to/module.js)
Optional name
the resolver name used in logs/debug output
Example
Here is most of the New Node resolver at the time of this writing. It is just a wrapper around unrs-resolver:
import module from 'node:module'
import path from 'node:path'
import { ResolverFactory } from 'unrs-resolver'
import type { NapiResolveOptions } from 'unrs-resolver'
import type { NewResolver } from './types.js'
export function createNodeResolver({
extensions = ['.mjs', '.cjs', '.js', '.json', '.node'],
conditionNames = ['import', 'require', 'default'],
mainFields = ['module', 'main'],
...restOptions
}: NapiResolveOptions = {}): NewResolver {
const resolver = new ResolverFactory({
extensions,
conditionNames,
mainFields,
...restOptions,
})
// shared context across all resolve calls
return {
interfaceVersion: 3,
name: 'eslint-plugin-import-x:node',
resolve(modulePath, sourceFile) {
if (module.isBuiltin(modulePath)) {
return { found: true, path: null }
}
if (modulePath.startsWith('data:')) {
return { found: true, path: null }
}
try {
const resolved = resolver.sync(path.dirname(sourceFile), modulePath)
if (resolved.path) {
return { found: true, path: resolved.path }
}
return { found: false }
} catch {
return { found: false }
}
},
}
}
v2
Resolvers must export two names:
interfaceVersion: number
The following document currently describes version 2 of the resolver interface. As such, a resolver implementing this version should
export const interfaceVersion = 2
or
exports.interfaceVersion = 2
resolve
Signature: (source: string, file: string, config?: unknown) => { found: boolean, path?: string | null }
Given:
// /some/path/to/module.js
import ... from './imported-file'
and
# .eslintrc.yml
---
settings:
import/resolver:
my-cool-resolver: [some, stuff]
node: { paths: [a, b, c] }
useRuleContext and getTsconfigWithContext
They are also available via eslint-import-context in the my-cool-resolver example, same as v3.
Arguments
The arguments provided will be:
source
the module identifier (./imported-file).
file
the absolute path to the file making the import (/some/path/to/module.js)
config
an object provided via the import/resolver setting. my-cool-resolver will get ["some", "stuff"] as its config, while
node will get { "paths": ["a", "b", "c"] } provided as config.
Example
Here is most of the Node resolver at the time of this writing. It is just a wrapper around substack/Browserify's synchronous resolve:
const resolve = require('resolve/sync')
const isCoreModule = require('is-core-module')
exports.resolve = function (source, file, config) {
if (isCoreModule(source)) {
return { found: true, path: null }
}
try {
return { found: true, path: resolve(source, opts(file, config)) }
} catch (err) {
return { found: false }
}
}
Shared resolve return value
The first resolver to return { found: true } is considered the source of truth. The returned object has:
found:trueif thesourcemodule can be resolved relative tofile, elsefalsepath: an absolute pathstringif the module can be located on the filesystem; else,null.
An example of a null path is a Node core module, such as fs or crypto. These modules can always be resolved, but the path need not be provided, as the plugin will not attempt to parse core modules at this time.
If the resolver cannot resolve source relative to file, it should just return { found: false }. No path key is needed in this case.