Skip to content

Commit 53b316c

Browse files
authored
feat: migrate enhanced-resolve to oxc-resolver (#237)
1 parent c8a388d commit 53b316c

12 files changed

+369
-61
lines changed

.changeset/dirty-rabbits-pretend.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": minor
3+
---
4+
5+
feat: migrate `enhanced-resolve` to `oxc-resolver`

.github/workflows/ci.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66

77
concurrency:
88
group: ${{ github.workflow }}-${{ github.ref }}
9-
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
9+
cancel-in-progress: true
1010

1111
jobs:
1212
ci:
@@ -30,6 +30,7 @@ jobs:
3030
- executeLint: true
3131
node: 20
3232
os: ubuntu-latest
33+
fail-fast: false
3334

3435
runs-on: ${{ matrix.os }}
3536
steps:

jest.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default {
1313
'^eslint-plugin-import-x/package.json$': `<rootDir>/package.json`,
1414
'^eslint-plugin-import-x/(.+)$': `<rootDir>/${srcDir}/$1`,
1515
},
16+
snapshotSerializers: ['<rootDir>/test/jest.serializer.ts'],
1617
testMatch: ['<rootDir>/test/**/*.spec.ts'],
1718
transform: {
1819
'^.+\\.(t|j)sx?$': ['@swc-node/jest', {} satisfies SwcOptions],

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@
5353
"@typescript-eslint/utils": "^8.1.0",
5454
"debug": "^4.3.4",
5555
"doctrine": "^3.0.0",
56-
"enhanced-resolve": "^5.17.1",
5756
"eslint-import-resolver-node": "^0.3.9",
5857
"get-tsconfig": "^4.7.3",
5958
"is-glob": "^4.0.3",
6059
"minimatch": "^9.0.3",
60+
"oxc-resolver": "^5.0.0",
6161
"semver": "^7.6.3",
6262
"stable-hash": "^0.0.4",
6363
"tslib": "^2.6.3"
@@ -117,6 +117,7 @@
117117
"jest": "^29.7.0",
118118
"klaw-sync": "^6.0.0",
119119
"npm-run-all2": "^6.1.2",
120+
"path-serializer": "^0.3.4",
120121
"prettier": "^3.2.5",
121122
"redux": "^5.0.1",
122123
"rimraf": "^5.0.10",

src/node-resolver.ts

+24-23
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,45 @@
1-
import fs from 'node:fs'
21
import { isBuiltin } from 'node:module'
32
import path from 'node:path'
43

5-
import { ResolverFactory, CachedInputFileSystem } from 'enhanced-resolve'
6-
import type { ResolveOptions } from 'enhanced-resolve'
4+
import { ResolverFactory } from 'oxc-resolver'
5+
import type { NapiResolveOptions as ResolveOptions } from 'oxc-resolver'
76

87
import type { NewResolver } from './types'
98

10-
type NodeResolverOptions = {
9+
export type NodeResolverOptions = {
1110
/**
12-
* The allowed extensions the resolver will attempt to find when resolving a module
13-
* @type {string[] | undefined}
11+
* Attempt to resolve these extensions in order.
12+
* If multiple files share the same name but have different extensions,
13+
* will resolve the one with the extension listed first in the array and skip the rest.
14+
*
1415
* @default ['.mjs', '.cjs', '.js', '.json', '.node']
1516
*/
1617
extensions?: string[]
1718
/**
18-
* The import conditions the resolver will used when reading the exports map from "package.json"
19-
* @type {string[] | undefined}
20-
* @default ['default', 'module', 'import', 'require']
19+
* Condition names for exports field which defines entry points of a package.
20+
* The key order in the exports field is significant. During condition matching, earlier entries have higher priority and take precedence over later entries.
21+
*
22+
* @default ['import', 'require', 'default']
2123
*/
2224
conditionNames?: string[]
23-
} & Omit<ResolveOptions, 'useSyncFileSystemCalls'>
25+
/**
26+
* A list of main fields in description files
27+
*
28+
* @default ['module', 'main']
29+
*/
30+
mainFields?: string[]
31+
} & ResolveOptions
2432

2533
export function createNodeResolver({
2634
extensions = ['.mjs', '.cjs', '.js', '.json', '.node'],
2735
conditionNames = ['import', 'require', 'default'],
2836
mainFields = ['module', 'main'],
29-
fileSystem = new CachedInputFileSystem(fs, 4 * 1000),
3037
...restOptions
31-
}: Partial<NodeResolverOptions> = {}): NewResolver {
32-
const resolver = ResolverFactory.createResolver({
38+
}: NodeResolverOptions = {}): NewResolver {
39+
const resolver = new ResolverFactory({
3340
extensions,
34-
fileSystem,
3541
conditionNames,
3642
mainFields,
37-
useSyncFileSystemCalls: true,
3843
...restOptions,
3944
})
4045

@@ -43,7 +48,7 @@ export function createNodeResolver({
4348
return {
4449
interfaceVersion: 3,
4550
name: 'eslint-plugin-import-x built-in node resolver',
46-
resolve: (modulePath, sourceFile) => {
51+
resolve(modulePath, sourceFile) {
4752
if (isBuiltin(modulePath)) {
4853
return { found: true, path: null }
4954
}
@@ -53,13 +58,9 @@ export function createNodeResolver({
5358
}
5459

5560
try {
56-
const resolved = resolver.resolveSync(
57-
{},
58-
path.dirname(sourceFile),
59-
modulePath,
60-
)
61-
if (resolved) {
62-
return { found: true, path: resolved }
61+
const resolved = resolver.sync(path.dirname(sourceFile), modulePath)
62+
if (resolved.path) {
63+
return { found: true, path: resolved.path }
6364
}
6465
return { found: false }
6566
} catch {

src/types.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
2-
import type { ResolveOptions } from 'enhanced-resolve'
32
import type { MinimatchOptions } from 'minimatch'
3+
import type { NapiResolveOptions as ResolveOptions } from 'oxc-resolver'
44
import type { KebabCase } from 'type-fest'
55

66
import type { ImportType as ImportType_, PluginName } from './utils'
@@ -40,7 +40,7 @@ export type NodeResolverOptions = {
4040
}
4141

4242
export type WebpackResolverOptions = {
43-
config?: string | { resolve: Omit<ResolveOptions, 'fileSystem'> }
43+
config?: string | { resolve: ResolveOptions }
4444
'config-index'?: number
4545
env?: Record<string, unknown>
4646
argv?: Record<string, unknown>
@@ -50,7 +50,7 @@ export type TsResolverOptions = {
5050
alwaysTryTypes?: boolean
5151
project?: string[] | string
5252
extensions?: string[]
53-
} & Omit<ResolveOptions, 'fileSystem' | 'useSyncFileSystemCalls'>
53+
} & ResolveOptions
5454

5555
// TODO: remove prefix New in the next major version
5656
export type NewResolverResolve = (

src/utils/parse.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function parse(
146146
if (!ast || typeof ast !== 'object') {
147147
console.warn(
148148
// Can only be invalid for custom parser per imports/parser
149-
`\`parseForESLint\` from parser \`${typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`'}\` is invalid and will just be ignored`,
149+
`\`parseForESLint\` from parser \`${typeof parserOrPath === 'string' ? parserOrPath : 'context.languageOptions.parser'}\` is invalid and will just be ignored`,
150150
{ content, parserMeta: parser.meta },
151151
)
152152
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`builtin node:path => true 1`] = `
4+
{
5+
"expected": true,
6+
"requireResolve": "node:path",
7+
"source": "node:path",
8+
}
9+
`;
10+
11+
exports[`builtin node:path => true 2`] = `
12+
{
13+
"expected": true,
14+
"result": {
15+
"found": true,
16+
"path": null,
17+
},
18+
"source": "node:path",
19+
}
20+
`;
21+
22+
exports[`builtin path => true 1`] = `
23+
{
24+
"expected": true,
25+
"requireResolve": "path",
26+
"source": "path",
27+
}
28+
`;
29+
30+
exports[`builtin path => true 2`] = `
31+
{
32+
"expected": true,
33+
"result": {
34+
"found": true,
35+
"path": null,
36+
},
37+
"source": "path",
38+
}
39+
`;
40+
41+
exports[`modules @sukka/does-not-exists => false 1`] = `
42+
{
43+
"expected": false,
44+
"requireResolve": undefined,
45+
"source": "@sukka/does-not-exists",
46+
}
47+
`;
48+
49+
exports[`modules @sukka/does-not-exists => false 2`] = `
50+
{
51+
"expected": false,
52+
"result": {
53+
"found": false,
54+
},
55+
"source": "@sukka/does-not-exists",
56+
}
57+
`;
58+
59+
exports[`modules jest => true 1`] = `
60+
{
61+
"expected": true,
62+
"requireResolve": "<ROOT>/node_modules/jest/build/index.js",
63+
"source": "jest",
64+
}
65+
`;
66+
67+
exports[`modules jest => true 2`] = `
68+
{
69+
"expected": true,
70+
"result": {
71+
"found": true,
72+
"path": "<ROOT>/node_modules/jest/build/index.js",
73+
},
74+
"source": "jest",
75+
}
76+
`;
77+
78+
exports[`relative ../.github/dependabot.yml => false 1`] = `
79+
{
80+
"expected": false,
81+
"requireResolve": undefined,
82+
"source": "../.github/dependabot.yml",
83+
}
84+
`;
85+
86+
exports[`relative ../.github/dependabot.yml => false 2`] = `
87+
{
88+
"expected": false,
89+
"result": {
90+
"found": false,
91+
},
92+
"source": "../.github/dependabot.yml",
93+
}
94+
`;
95+
96+
exports[`relative ../babel.config.js => babel.config.js 1`] = `
97+
{
98+
"expected": "babel.config.js",
99+
"requireResolve": "<ROOT>/babel.config.js",
100+
"source": "../babel.config.js",
101+
}
102+
`;
103+
104+
exports[`relative ../babel.config.js => babel.config.js 2`] = `
105+
{
106+
"expected": "babel.config.js",
107+
"result": {
108+
"found": true,
109+
"path": "<ROOT>/babel.config.js",
110+
},
111+
"source": "../babel.config.js",
112+
}
113+
`;
114+
115+
exports[`relative ../inexistent.js => false 1`] = `
116+
{
117+
"expected": false,
118+
"requireResolve": undefined,
119+
"source": "../inexistent.js",
120+
}
121+
`;
122+
123+
exports[`relative ../inexistent.js => false 2`] = `
124+
{
125+
"expected": false,
126+
"result": {
127+
"found": false,
128+
},
129+
"source": "../inexistent.js",
130+
}
131+
`;
132+
133+
exports[`relative ../package.json => package.json 1`] = `
134+
{
135+
"expected": "package.json",
136+
"requireResolve": "<ROOT>/package.json",
137+
"source": "../package.json",
138+
}
139+
`;
140+
141+
exports[`relative ../package.json => package.json 2`] = `
142+
{
143+
"expected": "package.json",
144+
"result": {
145+
"found": true,
146+
"path": "<ROOT>/package.json",
147+
},
148+
"source": "../package.json",
149+
}
150+
`;
151+
152+
exports[`relative ../test => test/index.js 1`] = `
153+
{
154+
"expected": "test/index.js",
155+
"requireResolve": "<ROOT>/test/index.js",
156+
"source": "../test",
157+
}
158+
`;
159+
160+
exports[`relative ../test => test/index.js 2`] = `
161+
{
162+
"expected": "test/index.js",
163+
"result": {
164+
"found": true,
165+
"path": "<ROOT>/test/index.js",
166+
},
167+
"source": "../test",
168+
}
169+
`;
170+
171+
exports[`relative ../test/ => test/index.js 1`] = `
172+
{
173+
"expected": "test/index.js",
174+
"requireResolve": "<ROOT>/test/index.js",
175+
"source": "../test/",
176+
}
177+
`;
178+
179+
exports[`relative ../test/ => test/index.js 2`] = `
180+
{
181+
"expected": "test/index.js",
182+
"result": {
183+
"found": true,
184+
"path": "<ROOT>/test/index.js",
185+
},
186+
"source": "../test/",
187+
}
188+
`;
189+
190+
exports[`relative ../test/index.js => test/index.js 1`] = `
191+
{
192+
"expected": "test/index.js",
193+
"requireResolve": "<ROOT>/test/index.js",
194+
"source": "../test/index.js",
195+
}
196+
`;
197+
198+
exports[`relative ../test/index.js => test/index.js 2`] = `
199+
{
200+
"expected": "test/index.js",
201+
"result": {
202+
"found": true,
203+
"path": "<ROOT>/test/index.js",
204+
},
205+
"source": "../test/index.js",
206+
}
207+
`;

test/jest.serializer.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createSnapshotSerializer } from 'path-serializer'
2+
3+
const serializer: ReturnType<typeof createSnapshotSerializer> =
4+
createSnapshotSerializer()
5+
6+
export = serializer

0 commit comments

Comments
 (0)