Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-sites-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-import-x": minor
---

feat: map legacy node resolver to the new one with fallback support
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ jobs:

include:
- executeLint: true
node: 22
node: lts/*
eslint: 9
os: ubuntu-latest
- noLegacyNodeResolver: true
node: lts/*
eslint: 9
os: ubuntu-latest
fail-fast: false
Expand All @@ -53,6 +57,10 @@ jobs:
- name: Install Dependencies
run: yarn --immutable

- name: Uninstall Legacy Node Resolver
if: ${{ matrix.noLegacyNodeResolver }}
run: yarn remove eslint-import-resolver-node

- name: Build and Test
run: yarn run-s test-compiled test

Expand Down
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,19 @@
"watch": "yarn test --watch"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
"eslint": "^8.57.0 || ^9.0.0",
"eslint-import-resolver-node": "*"
},
"peerDependenciesMeta": {
"eslint-import-resolver-node": {
"optional": true
}
},
"dependencies": {
"@typescript-eslint/utils": "^8.32.1",
"comment-parser": "^1.4.1",
"debug": "^4.4.1",
"eslint-import-context": "^0.1.5",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-context": "^0.1.6",
"is-glob": "^4.0.3",
"minimatch": "^9.0.3 || ^10.0.1",
"semver": "^7.7.2",
Expand Down Expand Up @@ -123,7 +128,8 @@
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-doc-generator": "^2.1.2",
"eslint-import-resolver-typescript": "^4.4.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^4.4.1",
"eslint-import-resolver-webpack": "^0.13.10",
"eslint-import-test-order-redirect": "link:./test/fixtures/order-redirect",
"eslint-plugin-eslint-plugin": "^6.4.0",
Expand Down
2 changes: 2 additions & 0 deletions src/utils/arraify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const arraify = <T>(value?: T | readonly T[]): T[] | undefined =>
value ? ((Array.isArray(value) ? value : [value]) as T[]) : undefined
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
LegacyImportResolver,
} from 'eslint-import-context'

export * from './arraify.js'
export * from './create-rule.js'
export * from './declared-scope.js'
export * from './docs-url.js'
Expand Down
26 changes: 21 additions & 5 deletions src/utils/legacy-resolver-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export function normalizeConfigResolvers(
for (const nameOrRecordOrObject of resolverArray) {
if (typeof nameOrRecordOrObject === 'string') {
const name = nameOrRecordOrObject

map.set(name, {
name,
enable: true,
Expand All @@ -64,26 +63,25 @@ export function normalizeConfigResolvers(
} else if (typeof nameOrRecordOrObject === 'object') {
if (nameOrRecordOrObject.name && nameOrRecordOrObject.resolver) {
const object = nameOrRecordOrObject as LegacyResolverObject

const { name, enable = true, options, resolver } = object
map.set(name, { name, enable, options, resolver })
} else {
const record = nameOrRecordOrObject as LegacyResolverRecord

for (const [name, enableOrOptions] of Object.entries(record)) {
const resolver = requireResolver(name, sourceFile)
if (typeof enableOrOptions === 'boolean') {
map.set(name, {
name,
enable: enableOrOptions,
options: undefined,
resolver: requireResolver(name, sourceFile),
resolver,
})
} else {
map.set(name, {
name,
enable: true,
options: enableOrOptions,
resolver: requireResolver(name, sourceFile),
resolver,
})
}
}
Expand All @@ -98,6 +96,18 @@ export function normalizeConfigResolvers(
return [...map.values()]
}

export const LEGACY_NODE_RESOLVERS = [
'node',
'eslint-import-resolver-node',
(function () {
try {
return cjsRequire.resolve('eslint-import-resolver-node')
} catch {
// ignore
}
})(),
].filter(Boolean)

function requireResolver(name: string, sourceFile: string) {
// Try to resolve package with conventional name
const resolver =
Expand All @@ -106,10 +116,16 @@ function requireResolver(name: string, sourceFile: string) {
tryRequire(path.resolve(getBaseDir(sourceFile), name))

if (!resolver) {
// ignore `node` resolver not found error, we'll use the new one instead
if (LEGACY_NODE_RESOLVERS.includes(name)) {
return undefined!
}

const err = new Error(`unable to load resolver "${name}".`)
err.name = IMPORT_RESOLVE_ERROR_NAME
throw err
}

if (!isLegacyResolverValid(resolver)) {
const err = new Error(`${name} with invalid interface loaded as resolver`)
err.name = IMPORT_RESOLVE_ERROR_NAME
Expand Down
132 changes: 124 additions & 8 deletions src/utils/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import { fileURLToPath } from 'node:url'
import { setRuleContext } from 'eslint-import-context'
import { stableHash } from 'stable-hash'

import { createNodeResolver } from '../node-resolver.js'
import { cjsRequire } from '../require.js'
import type {
ChildContext,
ImportSettings,
LegacyResolver,
NewResolver,
NodeResolverOptions,
PluginSettings,
RuleContext,
} from '../types.js'

import { arraify } from './arraify.js'
import { makeContextCacheKey } from './export-map.js'
import {
LEGACY_NODE_RESOLVERS,
normalizeConfigResolvers,
resolveWithLegacyResolver,
} from './legacy-resolver-settings.js'
Expand Down Expand Up @@ -114,6 +119,98 @@ function isValidNewResolver(resolver: unknown): resolver is NewResolver {
return true
}

function legacyNodeResolve(
resolverOptions: NodeResolverOptions,
context: ChildContext | RuleContext,
modulePath: string,
sourceFile: string,
) {
const {
extensions,
includeCoreModules,
moduleDirectory,
paths,
preserveSymlinks: symlinks,
package: packageJson,
packageFilter,
pathFilter,
packageIterator,
...rest
} = resolverOptions

const normalizedExtensions = arraify(extensions)

const modules = arraify(moduleDirectory)

const resolver = createNodeResolver({
extensions: normalizedExtensions,
builtinModules: includeCoreModules !== false,
modules,
symlinks,
...rest,
})

const resolved = setRuleContext(context, () =>
resolver.resolve(modulePath, sourceFile),
)

if (resolved.found) {
return resolved
}

const normalizedPaths = arraify(paths)

if (normalizedPaths?.length) {
const paths = modules?.length
? normalizedPaths.filter(p => !modules.includes(p))
: normalizedPaths

if (paths.length > 0) {
const resolver = createNodeResolver({
extensions: normalizedExtensions,
builtinModules: includeCoreModules !== false,
modules: paths,
symlinks,
...rest,
})

const resolved = setRuleContext(context, () =>
resolver.resolve(modulePath, sourceFile),
)

if (resolved.found) {
return resolved
}
}
}

if (
[packageJson, packageFilter, pathFilter, packageIterator].some(
it => it != null,
)
) {
let legacyNodeResolver: LegacyResolver
try {
legacyNodeResolver = cjsRequire<LegacyResolver>(
'eslint-import-resolver-node',
)
} catch {
throw new Error(
"You're using legacy resolver options which are not supported by the new resolver and `eslint-import-resolver-node` is not installed as fallback,\nplease install `eslint-import-resolver-node` manually or remove those legacy resolver options including `package`, `packageFilter`, `pathFilter` and `packageIterator`.",
)
}
const resolved = resolveWithLegacyResolver(
legacyNodeResolver,
resolverOptions,
modulePath,
sourceFile,
)
if (resolved.found) {
return resolved
}
}
}

function fullResolve(
modulePath: string,
sourceFile: string,
Expand All @@ -140,11 +237,11 @@ function fullResolve(

const cacheKey =
sourceDir +
',' +
'\0' +
childContextHashKey +
',' +
'\0' +
memoizedHash +
',' +
'\0' +
modulePath

const cacheSettings = ModuleCache.getSettings(settings)
Expand All @@ -154,10 +251,7 @@ function fullResolve(
return { found: true, path: cachedPath }
}

if (
Object.hasOwn(settings, 'import-x/resolver-next') &&
settings['import-x/resolver-next']
) {
if (settings['import-x/resolver-next']) {
let configResolvers = settings['import-x/resolver-next']

if (!Array.isArray(configResolvers)) {
Expand Down Expand Up @@ -196,14 +290,36 @@ function fullResolve(
node: settings['import-x/resolve'],
} // backward compatibility

for (const { enable, options, resolver } of normalizeConfigResolvers(
for (const { enable, name, options, resolver } of normalizeConfigResolvers(
configResolvers,
sourceFile,
)) {
if (!enable) {
continue
}

// if the resolver is `eslint-import-resolver-node`, we use the new `node` resolver as fallback instead
if (LEGACY_NODE_RESOLVERS.includes(name) && !resolver) {
const resolverOptions = (options || {}) as NodeResolverOptions
const resolved = legacyNodeResolve(
{
...resolverOptions,
extensions:
resolverOptions.extensions || settings['import-x/extensions'],
},
context,
modulePath,
sourceFile,
)

if (resolved?.found) {
fileExistsCache.set(cacheKey, resolved.path)
return resolved
}

continue
}

const resolved = setRuleContext(context, () =>
resolveWithLegacyResolver(resolver, options, modulePath, sourceFile),
)
Expand Down
Loading
Loading