Skip to content

Commit 43cdd21

Browse files
SukkaWJounQin
andauthored
feat: map legacy node resolver to the new one with fallback support (#272)
Co-authored-by: JounQin <[email protected]>
1 parent 3bc48fc commit 43cdd21

File tree

11 files changed

+316
-156
lines changed

11 files changed

+316
-156
lines changed

.changeset/khaki-sites-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": minor
3+
---
4+
5+
feat: map legacy node resolver to the new one with fallback support

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ jobs:
2929

3030
include:
3131
- executeLint: true
32-
node: 22
32+
node: lts/*
33+
eslint: 9
34+
os: ubuntu-latest
35+
- legacyNodeResolver: true
36+
node: lts/*
3337
eslint: 9
3438
os: ubuntu-latest
3539
fail-fast: false
@@ -53,6 +57,10 @@ jobs:
5357
- name: Install Dependencies
5458
run: yarn --immutable
5559

60+
- name: Install Legacy Node Resolver
61+
if: ${{ matrix.legacyNodeResolver }}
62+
run: yarn add -D eslint-import-resolver-node
63+
5664
- name: Build and Test
5765
run: yarn run-s test-compiled test
5866

package.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,25 @@
6464
"watch": "yarn test --watch"
6565
},
6666
"peerDependencies": {
67-
"eslint": "^8.57.0 || ^9.0.0"
67+
"eslint": "^8.57.0 || ^9.0.0",
68+
"eslint-import-resolver-node": "*"
69+
},
70+
"peerDependenciesMeta": {
71+
"eslint-import-resolver-node": {
72+
"optional": true
73+
}
6874
},
6975
"dependencies": {
7076
"@typescript-eslint/utils": "^8.32.1",
7177
"comment-parser": "^1.4.1",
7278
"debug": "^4.4.1",
73-
"eslint-import-context": "^0.1.5",
74-
"eslint-import-resolver-node": "^0.3.9",
79+
"eslint-import-context": "^0.1.6",
7580
"is-glob": "^4.0.3",
7681
"minimatch": "^9.0.3 || ^10.0.1",
7782
"semver": "^7.7.2",
7883
"stable-hash": "^0.0.5",
7984
"tslib": "^2.8.1",
80-
"unrs-resolver": "^1.7.2"
85+
"unrs-resolver": "^1.7.5"
8186
},
8287
"devDependencies": {
8388
"@1stg/commitlint-config": "^5.0.6",
@@ -123,7 +128,7 @@
123128
"eslint": "^9.27.0",
124129
"eslint-config-prettier": "^10.1.5",
125130
"eslint-doc-generator": "^2.1.2",
126-
"eslint-import-resolver-typescript": "^4.4.0",
131+
"eslint-import-resolver-typescript": "^4.4.1",
127132
"eslint-import-resolver-webpack": "^0.13.10",
128133
"eslint-import-test-order-redirect": "link:./test/fixtures/order-redirect",
129134
"eslint-plugin-eslint-plugin": "^6.4.0",

src/rules/no-named-as-default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default createRule<[], MessageId>({
4040
declaration.source.value,
4141
context,
4242
)
43+
4344
if (exportMapOfImported == null) {
4445
return
4546
}

src/utils/arraify.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const arraify = <T>(value?: T | readonly T[]): T[] | undefined =>
2+
value ? ((Array.isArray(value) ? value : [value]) as T[]) : undefined

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type {
1010
LegacyImportResolver,
1111
} from 'eslint-import-context'
1212

13+
export * from './arraify.js'
1314
export * from './create-rule.js'
1415
export * from './declared-scope.js'
1516
export * from './docs-url.js'

src/utils/legacy-resolver-settings.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export function normalizeConfigResolvers(
5454
for (const nameOrRecordOrObject of resolverArray) {
5555
if (typeof nameOrRecordOrObject === 'string') {
5656
const name = nameOrRecordOrObject
57-
5857
map.set(name, {
5958
name,
6059
enable: true,
@@ -64,26 +63,25 @@ export function normalizeConfigResolvers(
6463
} else if (typeof nameOrRecordOrObject === 'object') {
6564
if (nameOrRecordOrObject.name && nameOrRecordOrObject.resolver) {
6665
const object = nameOrRecordOrObject as LegacyResolverObject
67-
6866
const { name, enable = true, options, resolver } = object
6967
map.set(name, { name, enable, options, resolver })
7068
} else {
7169
const record = nameOrRecordOrObject as LegacyResolverRecord
72-
7370
for (const [name, enableOrOptions] of Object.entries(record)) {
71+
const resolver = requireResolver(name, sourceFile)
7472
if (typeof enableOrOptions === 'boolean') {
7573
map.set(name, {
7674
name,
7775
enable: enableOrOptions,
7876
options: undefined,
79-
resolver: requireResolver(name, sourceFile),
77+
resolver,
8078
})
8179
} else {
8280
map.set(name, {
8381
name,
8482
enable: true,
8583
options: enableOrOptions,
86-
resolver: requireResolver(name, sourceFile),
84+
resolver,
8785
})
8886
}
8987
}
@@ -98,6 +96,17 @@ export function normalizeConfigResolvers(
9896
return [...map.values()]
9997
}
10098

99+
export const LEGACY_NODE_RESOLVERS = new Set([
100+
'node',
101+
'eslint-import-resolver-node',
102+
])
103+
104+
try {
105+
LEGACY_NODE_RESOLVERS.add(cjsRequire.resolve('eslint-import-resolver-node'))
106+
} catch {
107+
// ignore
108+
}
109+
101110
function requireResolver(name: string, sourceFile: string) {
102111
// Try to resolve package with conventional name
103112
const resolver =
@@ -106,10 +115,16 @@ function requireResolver(name: string, sourceFile: string) {
106115
tryRequire(path.resolve(getBaseDir(sourceFile), name))
107116

108117
if (!resolver) {
118+
// ignore `node` resolver not found error, we'll use the new one instead
119+
if (LEGACY_NODE_RESOLVERS.has(name)) {
120+
return undefined!
121+
}
122+
109123
const err = new Error(`unable to load resolver "${name}".`)
110124
err.name = IMPORT_RESOLVE_ERROR_NAME
111125
throw err
112126
}
127+
113128
if (!isLegacyResolverValid(resolver)) {
114129
const err = new Error(`${name} with invalid interface loaded as resolver`)
115130
err.name = IMPORT_RESOLVE_ERROR_NAME

src/utils/resolve.ts

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import { fileURLToPath } from 'node:url'
55
import { setRuleContext } from 'eslint-import-context'
66
import { stableHash } from 'stable-hash'
77

8+
import { createNodeResolver } from '../node-resolver.js'
9+
import { cjsRequire } from '../require.js'
810
import type {
911
ChildContext,
1012
ImportSettings,
1113
LegacyResolver,
1214
NewResolver,
15+
NodeResolverOptions,
1316
PluginSettings,
1417
RuleContext,
1518
} from '../types.js'
1619

20+
import { arraify } from './arraify.js'
1721
import { makeContextCacheKey } from './export-map.js'
1822
import {
23+
LEGACY_NODE_RESOLVERS,
1924
normalizeConfigResolvers,
2025
resolveWithLegacyResolver,
2126
} from './legacy-resolver-settings.js'
@@ -114,6 +119,106 @@ function isValidNewResolver(resolver: unknown): resolver is NewResolver {
114119
return true
115120
}
116121

122+
function legacyNodeResolve(
123+
resolverOptions: NodeResolverOptions,
124+
context: ChildContext | RuleContext,
125+
modulePath: string,
126+
sourceFile: string,
127+
) {
128+
const {
129+
extensions,
130+
includeCoreModules,
131+
moduleDirectory,
132+
paths,
133+
preserveSymlinks,
134+
package: packageJson,
135+
packageFilter,
136+
pathFilter,
137+
packageIterator,
138+
...rest
139+
} = resolverOptions
140+
141+
const normalizedExtensions = arraify(extensions)
142+
143+
const modules = arraify(moduleDirectory)
144+
145+
// TODO: change the default behavior to align node itself
146+
const symlinks = preserveSymlinks === false
147+
148+
const resolver = createNodeResolver({
149+
extensions: normalizedExtensions,
150+
builtinModules: includeCoreModules !== false,
151+
modules,
152+
symlinks,
153+
...rest,
154+
})
155+
156+
const resolved = setRuleContext(context, () =>
157+
resolver.resolve(modulePath, sourceFile),
158+
)
159+
160+
if (resolved.found) {
161+
return resolved
162+
}
163+
164+
const normalizedPaths = arraify(paths)
165+
166+
if (normalizedPaths?.length) {
167+
const paths = modules?.length
168+
? normalizedPaths.filter(p => !modules.includes(p))
169+
: normalizedPaths
170+
171+
if (paths.length > 0) {
172+
const resolver = createNodeResolver({
173+
extensions: normalizedExtensions,
174+
builtinModules: includeCoreModules !== false,
175+
modules: paths,
176+
symlinks,
177+
...rest,
178+
})
179+
180+
const resolved = setRuleContext(context, () =>
181+
resolver.resolve(modulePath, sourceFile),
182+
)
183+
184+
if (resolved.found) {
185+
return resolved
186+
}
187+
}
188+
}
189+
190+
if (
191+
[packageJson, packageFilter, pathFilter, packageIterator].some(
192+
it => it != null,
193+
)
194+
) {
195+
let legacyNodeResolver: LegacyResolver
196+
try {
197+
legacyNodeResolver = cjsRequire<LegacyResolver>(
198+
'eslint-import-resolver-node',
199+
)
200+
} catch {
201+
throw new Error(
202+
[
203+
"You're using legacy resolver options which are not supported by the new resolver.",
204+
'Please either:',
205+
'1. Install `eslint-import-resolver-node` as a fallback, or',
206+
'2. Remove legacy options: `package`, `packageFilter`, `pathFilter`, `packageIterator`',
207+
].join('\n'),
208+
)
209+
}
210+
const resolved = resolveWithLegacyResolver(
211+
legacyNodeResolver,
212+
resolverOptions,
213+
modulePath,
214+
sourceFile,
215+
)
216+
if (resolved.found) {
217+
return resolved
218+
}
219+
}
220+
}
221+
117222
function fullResolve(
118223
modulePath: string,
119224
sourceFile: string,
@@ -140,11 +245,11 @@ function fullResolve(
140245

141246
const cacheKey =
142247
sourceDir +
143-
',' +
248+
'\0' +
144249
childContextHashKey +
145-
',' +
250+
'\0' +
146251
memoizedHash +
147-
',' +
252+
'\0' +
148253
modulePath
149254

150255
const cacheSettings = ModuleCache.getSettings(settings)
@@ -154,10 +259,7 @@ function fullResolve(
154259
return { found: true, path: cachedPath }
155260
}
156261

157-
if (
158-
Object.hasOwn(settings, 'import-x/resolver-next') &&
159-
settings['import-x/resolver-next']
160-
) {
262+
if (settings['import-x/resolver-next']) {
161263
let configResolvers = settings['import-x/resolver-next']
162264

163265
if (!Array.isArray(configResolvers)) {
@@ -196,14 +298,41 @@ function fullResolve(
196298
node: settings['import-x/resolve'],
197299
} // backward compatibility
198300

199-
for (const { enable, options, resolver } of normalizeConfigResolvers(
301+
for (const { enable, name, options, resolver } of normalizeConfigResolvers(
200302
configResolvers,
201303
sourceFile,
202304
)) {
203305
if (!enable) {
204306
continue
205307
}
206308

309+
// if the resolver is `eslint-import-resolver-node`, we use the new `node` resolver first
310+
// and try `eslint-import-resolver-node` as fallback instead
311+
if (LEGACY_NODE_RESOLVERS.has(name)) {
312+
const resolverOptions = (options || {}) as NodeResolverOptions
313+
const resolved = legacyNodeResolve(
314+
resolverOptions,
315+
// TODO: enable the following in the next major
316+
// {
317+
// ...resolverOptions,
318+
// extensions:
319+
// resolverOptions.extensions || settings['import-x/extensions'],
320+
// },
321+
context,
322+
modulePath,
323+
sourceFile,
324+
)
325+
326+
if (resolved?.found) {
327+
fileExistsCache.set(cacheKey, resolved.path)
328+
return resolved
329+
}
330+
331+
if (!resolver) {
332+
continue
333+
}
334+
}
335+
207336
const resolved = setRuleContext(context, () =>
208337
resolveWithLegacyResolver(resolver, options, modulePath, sourceFile),
209338
)

test/__snapshots__/node-resolver.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ exports[`modules jest => true 2`] = `
6969
"expected": true,
7070
"result": {
7171
"found": true,
72-
"path": "<ROOT>/node_modules/jest/build/index.js",
72+
"path": "<ROOT>/node_modules/jest/build/index.mjs",
7373
},
7474
"source": "jest",
7575
}

0 commit comments

Comments
 (0)