Skip to content

Commit 1b2df1a

Browse files
committed
feat: implement a new options argument for resolver with 100% compatibility
1 parent 685477f commit 1b2df1a

12 files changed

+180
-57
lines changed

.changeset/four-pumas-leave.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: implement a new `options` argument for `resolver` with 100% compatibility

resolvers/README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Currently, version 1 is assumed if no `interfaceVersion` is available. (didn't t
1010
- [Arguments](#arguments)
1111
- [`source`](#source)
1212
- [`file`](#file)
13+
- [`options`](#options)
14+
- [`options.context`](#optionscontext)
15+
- [`options.tsconfig`](#optionstsconfig)
1316
- [Optional `name`](#optional-name)
1417
- [Example](#example)
1518
- [v2](#v2)
@@ -19,6 +22,8 @@ Currently, version 1 is assumed if no `interfaceVersion` is available. (didn't t
1922
- [`source`](#source-1)
2023
- [`file`](#file-1)
2124
- [`config`](#config)
25+
- [`undefined`](#undefined)
26+
- [`options`](#options-1)
2227
- [Example](#example-1)
2328
- [Shared `resolve` return value](#shared-resolve-return-value)
2429

@@ -42,7 +47,7 @@ exports.interfaceVersion = 3
4247

4348
### Required `resolve`
4449

45-
Signature: `(source: string, file: string) => { found: boolean, path?: string | null }`
50+
Signature: `(source: string, file: string, options: { context: ChildContext | RuleContext, tsconfig?: TsConfigJsonResolved }) => { found: boolean, path?: string | null }`
4651

4752
Given:
4853

@@ -64,7 +69,7 @@ export default [
6469
{
6570
name: 'my-cool-resolver',
6671
interfaceVersion: 3,
67-
resolve(source, file) {
72+
resolve(source, file, options) {
6873
// use a factory to get config outside of the resolver
6974
},
7075
},
@@ -89,6 +94,22 @@ the module identifier (`./imported-file`).
8994

9095
the absolute path to the file making the import (`/some/path/to/module.js`)
9196

97+
##### `options`
98+
99+
###### `options.context`
100+
101+
> [!NOTE]
102+
> Only available after `[email protected]+`
103+
104+
Please view [ChildContext] and [RuleContext] for more details.
105+
106+
###### `options.tsconfig`
107+
108+
> [!NOTE]
109+
> Only available after `[email protected]+`
110+
111+
Please view [TsConfigJsonResolved] for more details.
112+
92113
### Optional `name`
93114

94115
the resolver name used in logs/debug output
@@ -204,6 +225,14 @@ the absolute path to the file making the import (`/some/path/to/module.js`)
204225
an object provided via the `import/resolver` setting. `my-cool-resolver` will get `["some", "stuff"]` as its `config`, while
205226
`node` will get `{ "paths": ["a", "b", "c"] }` provided as `config`.
206227

228+
##### `undefined`
229+
230+
For compatibility reason, the 4th argument is always passed as `undefined`. Take [TypeScript resolver](https://github.com/import-js/eslint-import-resolver-typescript/blob/c45039e5c310479c1e178c2180e054380facbadd/src/index.ts#L69) for example. It uses the 4th argument to pass the optional [unrs-resolver] `ResolverFactory` instance to support both `v2` and `v3` interface at the same time.
231+
232+
##### `options`
233+
234+
Same as [options](#options) in `v3` resolver above
235+
207236
### Example
208237

209238
Here is most of the [Node resolver] at the time of this writing. It is just a wrapper around substack/Browserify's synchronous [`resolve`][resolve]:
@@ -212,7 +241,7 @@ Here is most of the [Node resolver] at the time of this writing. It is just a wr
212241
var resolve = require('resolve/sync')
213242
var isCoreModule = require('is-core-module')
214243
215-
exports.resolve = function (source, file, config) {
244+
exports.resolve = function (source, file, config, _, options) {
216245
if (isCoreModule(source)) return { found: true, path: null }
217246
try {
218247
return { found: true, path: resolve(source, opts(file, config)) }
@@ -237,3 +266,6 @@ If the resolver cannot resolve `source` relative to `file`, it should just retur
237266
[Node resolver]: https://github.com/import-js/eslint-plugin-import/blob/main/resolvers/node/index.js
238267
[resolve]: https://www.npmjs.com/package/resolve
239268
[unrs-resolver]: https://www.npmjs.com/package/unrs-resolver
269+
[ChildContext]: https://github.com/un-ts/eslint-plugin-import-x/blob/685477fd509a3b5a91d68f349735198a4cf70020/src/types.ts#L137
270+
[RuleContext]: https://eslint.org/docs/latest/extend/custom-rules#the-context-object
271+
[TsConfigJsonResolved]: https://github.com/privatenumber/get-tsconfig/blob/8564f8821efa26cc53d2d60b2f63c013969dbe49/src/types.ts#L5C13-L5C33

src/types.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
2+
import type { TsConfigJsonResolved } from 'get-tsconfig'
23
import type { MinimatchOptions } from 'minimatch'
34
import type { KebabCase } from 'type-fest'
4-
import type { NapiResolveOptions as ResolveOptions } from 'unrs-resolver'
5+
import type { NapiResolveOptions } from 'unrs-resolver'
56

67
import type {
78
ImportType as ImportType_,
@@ -41,22 +42,31 @@ export interface NodeResolverOptions {
4142
}
4243

4344
export interface WebpackResolverOptions {
44-
config?: string | { resolve: ResolveOptions }
45+
config?: string | { resolve: NapiResolveOptions }
4546
'config-index'?: number
4647
env?: Record<string, unknown>
4748
argv?: Record<string, unknown>
4849
}
4950

50-
export interface TsResolverOptions extends ResolveOptions {
51+
export interface TsResolverOptions extends NapiResolveOptions {
5152
alwaysTryTypes?: boolean
5253
project?: string[] | string
5354
extensions?: string[]
5455
}
5556

57+
export interface ResolveOptionsExtra {
58+
tsconfig?: TsConfigJsonResolved
59+
}
60+
61+
export interface ResolveOptions extends ResolveOptionsExtra {
62+
context: ChildContext | RuleContext
63+
}
64+
5665
// TODO: remove prefix New in the next major version
5766
export type NewResolverResolve = (
5867
modulePath: string,
5968
sourceFile: string,
69+
options: ResolveOptions,
6070
) => ResolvedResult
6171

6272
// TODO: remove prefix New in the next major version
@@ -141,6 +151,7 @@ export interface ChildContext {
141151
parserOptions?: TSESLint.ParserOptions
142152
languageOptions?: TSESLint.FlatConfig.LanguageOptions
143153
path: string
154+
cwd: string
144155
filename?: string
145156
}
146157

src/utils/export-map.ts

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,40 @@ const parseComment = (comment: string): commentParser.Block => {
9090
}
9191
}
9292

93+
function getTsconfigInternal(context: ChildContext | RuleContext) {
94+
const parserOptions = context.parserOptions || {}
95+
let tsconfigRootDir = parserOptions.tsconfigRootDir
96+
const project = parserOptions.project
97+
const cacheKey = stableHash({ tsconfigRootDir, project })
98+
let tsConfig: TsConfigJsonResolved | null | undefined
99+
100+
if (tsconfigCache.has(cacheKey)) {
101+
tsConfig = tsconfigCache.get(cacheKey)!
102+
} else {
103+
tsconfigRootDir = tsconfigRootDir || process.cwd()
104+
let tsconfigResult: TsConfigResult | null | undefined
105+
if (project) {
106+
const projects = Array.isArray(project) ? project : [project]
107+
for (const project of projects) {
108+
tsconfigResult = getTsconfig(
109+
project === true
110+
? context.filename
111+
: path.resolve(tsconfigRootDir, project),
112+
)
113+
if (tsconfigResult) {
114+
break
115+
}
116+
}
117+
} else {
118+
tsconfigResult = getTsconfig(tsconfigRootDir)
119+
}
120+
tsConfig = tsconfigResult?.config
121+
tsconfigCache.set(cacheKey, tsConfig)
122+
}
123+
124+
return tsConfig
125+
}
126+
93127
export class ExportMap {
94128
static for(context: ChildContext) {
95129
const filepath = context.path
@@ -161,7 +195,13 @@ export class ExportMap {
161195
}
162196

163197
static get(source: string, context: RuleContext) {
164-
const path = resolve(source, context)
198+
const tsconfig = lazy(() => getTsconfigInternal(context))
199+
200+
const path = resolve(source, context, {
201+
get tsconfig() {
202+
return tsconfig()
203+
},
204+
})
165205
if (path == null) {
166206
return null
167207
}
@@ -171,7 +211,12 @@ export class ExportMap {
171211

172212
static parse(filepath: string, content: string, context: ChildContext) {
173213
const m = new ExportMap(filepath)
174-
const isEsModuleInteropTrue = lazy(isEsModuleInterop)
214+
215+
const tsconfig = lazy(() => getTsconfigInternal(context))
216+
217+
const isEsModuleInteropTrue = lazy(
218+
() => tsconfig()?.compilerOptions?.esModuleInterop ?? false,
219+
)
175220

176221
let ast: TSESTree.Program
177222
let visitorKeys: TSESLint.SourceCode.VisitorKeys | null
@@ -243,7 +288,11 @@ export class ExportMap {
243288
const namespaces = new Map</* identifier */ string, /* source */ string>()
244289

245290
function remotePath(value: string) {
246-
return relative(value, filepath, context.settings)
291+
return relative(value, filepath, context.settings, context, {
292+
get tsconfig() {
293+
return tsconfig()
294+
},
295+
})
247296
}
248297

249298
function resolveImport(value: string) {
@@ -428,40 +477,6 @@ export class ExportMap {
428477

429478
const source = new SourceCode({ text: content, ast: ast as AST.Program })
430479

431-
function isEsModuleInterop() {
432-
const parserOptions = context.parserOptions || {}
433-
let tsconfigRootDir = parserOptions.tsconfigRootDir
434-
const project = parserOptions.project
435-
const cacheKey = stableHash({ tsconfigRootDir, project })
436-
let tsConfig: TsConfigJsonResolved | null | undefined
437-
438-
if (tsconfigCache.has(cacheKey)) {
439-
tsConfig = tsconfigCache.get(cacheKey)!
440-
} else {
441-
tsconfigRootDir = tsconfigRootDir || process.cwd()
442-
let tsconfigResult: TsConfigResult | null | undefined
443-
if (project) {
444-
const projects = Array.isArray(project) ? project : [project]
445-
for (const project of projects) {
446-
tsconfigResult = getTsconfig(
447-
project === true
448-
? context.filename
449-
: path.resolve(tsconfigRootDir, project),
450-
)
451-
if (tsconfigResult) {
452-
break
453-
}
454-
}
455-
} else {
456-
tsconfigResult = getTsconfig(tsconfigRootDir)
457-
}
458-
tsConfig = tsconfigResult?.config
459-
tsconfigCache.set(cacheKey, tsConfig)
460-
}
461-
462-
return tsConfig?.compilerOptions?.esModuleInterop ?? false
463-
}
464-
465480
for (const n of ast.body) {
466481
if (n.type === 'ExportDefaultDeclaration') {
467482
const exportMeta = captureDoc(source, docStyleParsers, n)
@@ -1146,6 +1161,7 @@ function childContext(
11461161
parserPath,
11471162
languageOptions,
11481163
path,
1164+
cwd: context.cwd,
11491165
filename:
11501166
'physicalFilename' in context
11511167
? context.physicalFilename

src/utils/legacy-resolver-settings.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import type { LiteralUnion } from 'type-fest'
77

88
import { cjsRequire } from '../require.js'
99
import type {
10+
ChildContext,
1011
NodeResolverOptions,
1112
ResolvedResult,
13+
ResolveOptions,
14+
ResolveOptionsExtra,
15+
RuleContext,
1216
TsResolverOptions,
1317
WebpackResolverOptions,
1418
} from '../types.js'
@@ -25,12 +29,16 @@ export type LegacyResolverResolveImport<T = unknown> = (
2529
modulePath: string,
2630
sourceFile: string,
2731
config: T,
32+
_: undefined,
33+
options: ResolveOptions,
2834
) => string | undefined
2935

3036
export type LegacyResolverResolve<T = unknown> = (
3137
modulePath: string,
3238
sourceFile: string,
3339
config: T,
40+
_: undefined,
41+
options: ResolveOptions,
3442
) => ResolvedResult
3543

3644
export interface LegacyResolver<T = unknown, U = T> {
@@ -77,13 +85,23 @@ export function resolveWithLegacyResolver(
7785
config: unknown,
7886
modulePath: string,
7987
sourceFile: string,
88+
context: ChildContext | RuleContext,
89+
extra: ResolveOptionsExtra,
8090
): ResolvedResult {
91+
const options = { context, ...extra }
92+
8193
if (resolver.interfaceVersion === 2) {
82-
return resolver.resolve(modulePath, sourceFile, config)
94+
return resolver.resolve(modulePath, sourceFile, config, undefined, options)
8395
}
8496

8597
try {
86-
const resolved = resolver.resolveImport(modulePath, sourceFile, config)
98+
const resolved = resolver.resolveImport(
99+
modulePath,
100+
sourceFile,
101+
config,
102+
undefined,
103+
options,
104+
)
87105
if (resolved === undefined) {
88106
return {
89107
found: false,

0 commit comments

Comments
 (0)