Skip to content

Commit

Permalink
feat: better algorithm for custom tsconfig paths (#345)
Browse files Browse the repository at this point in the history
* test(fix): tsconfig include typo

* feat: better algorithm for custom tsconfig paths

* perf: ignore mapper if files is empty

* chore: increase size limit to 3kB

* chore: changeset
  • Loading branch information
carlocorradini authored Feb 14, 2025
1 parent c9cfe70 commit fcc8883
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-kiwis-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-import-resolver-typescript': minor
---

Enable the mapper function just for a set of allowed files. Improves project discovery using glob and POSIX separator.
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
{
"path": "./lib/index.js",
"limit": "2.8kB"
"limit": "3kB"
}
]
197 changes: 154 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ let cachedOptions: InternalResolverOptions | undefined
let prevCwd: string

let mappersCachedOptions: InternalResolverOptions
let mappers: Array<((specifier: string) => string[]) | null> | undefined
let mappers: Array<{
files: Set<string>
mapperFn: NonNullable<ReturnType<typeof createPathsMatcher>>
}> = []

let resolverCachedOptions: InternalResolverOptions
let cachedResolver: Resolver | undefined
Expand Down Expand Up @@ -159,7 +162,7 @@ export function resolve(
resolver = cachedResolver
}

log('looking for:', source)
log('looking for', source, 'in', file)

source = removeQuerystring(source)

Expand Down Expand Up @@ -300,34 +303,35 @@ function getMappedPath(
paths = [resolved]
}
} else {
paths = mappers!
.map(mapper =>
mapper?.(source).map(item => [
...extensions.map(ext => `${item}${ext}`),
...originalExtensions.map(ext => `${item}/index${ext}`),
]),
)
.flat(2)
.filter(mappedPath => {
if (mappedPath === undefined) {
return false
}

try {
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false })
if (stat === undefined) return false
if (stat.isFile()) return true

// Maybe this is a module dir?
if (stat.isDirectory()) {
return isModule(mappedPath)
}
} catch {
return false
paths = [
...new Set(
mappers
.filter(({ files }) => files.has(file))
.map(({ mapperFn }) =>
mapperFn(source).map(item => [
...extensions.map(ext => `${item}${ext}`),
...originalExtensions.map(ext => `${item}/index${ext}`),
]),
)
.flat(2)
.map(toNativePathSeparator),
),
].filter(mappedPath => {
try {
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false })
if (stat === undefined) return false
if (stat.isFile()) return true

// Maybe this is a module dir?
if (stat.isDirectory()) {
return isModule(mappedPath)
}

} catch {
return false
})
}

return false
})
}

if (retry && paths.length === 0) {
Expand Down Expand Up @@ -367,50 +371,114 @@ function getMappedPath(
return paths[0]
}

// eslint-disable-next-line sonarjs/cognitive-complexity
function initMappers(options: InternalResolverOptions) {
if (
mappers &&
mappers.length > 0 &&
mappersCachedOptions === options &&
prevCwd === process.cwd()
) {
return
}
prevCwd = process.cwd()

const configPaths =
const configPaths = (
typeof options.project === 'string'
? [options.project]
: Array.isArray(options.project)
? options.project
: [process.cwd()]
) // 'tinyglobby' pattern must have POSIX separator
.map(config => replacePathSeparator(config, path.sep, path.posix.sep))

const ignore = ['!**/node_modules/**']
// https://github.com/microsoft/TypeScript/blob/df342b7206cb56b56bb3b3aecbb2ee2d2ff7b217/src/compiler/commandLineParser.ts#L3006
const defaultInclude = ['**/*']
const defaultIgnore = ['**/node_modules/**']

// turn glob patterns into paths
// Turn glob patterns into paths
const projectPaths = [
...new Set([
...configPaths.filter(path => !isDynamicPattern(path)),
...globSync(
[...configPaths.filter(path => isDynamicPattern(path)), ...ignore],
configPaths.filter(path => isDynamicPattern(path)),
{
expandDirectories: false,
ignore: defaultIgnore,
absolute: true,
},
),
]),
]

mappers = projectPaths.map(projectPath => {
let tsconfigResult: TsConfigResult | null
mappers = projectPaths
.map(projectPath => {
let tsconfigResult: TsConfigResult | null

if (isFile(projectPath)) {
const { dir, base } = path.parse(projectPath)
tsconfigResult = getTsconfig(dir, base)
} else {
tsconfigResult = getTsconfig(projectPath)
}
if (isFile(projectPath)) {
const { dir, base } = path.parse(projectPath)
tsconfigResult = getTsconfig(dir, base)
} else {
tsconfigResult = getTsconfig(projectPath)
}

return tsconfigResult && createPathsMatcher(tsconfigResult)
})
if (!tsconfigResult) {
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined
}

const mapperFn = createPathsMatcher(tsconfigResult)

if (!mapperFn) {
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined
}

const files =
tsconfigResult.config.files === undefined &&
tsconfigResult.config.include === undefined
? // Include everything if no files or include options
globSync(defaultInclude, {
ignore: [
...(tsconfigResult.config.exclude ?? []),
...defaultIgnore,
],
absolute: true,
cwd: path.dirname(tsconfigResult.path),
})
: [
// https://www.typescriptlang.org/tsconfig/#files
...(tsconfigResult.config.files !== undefined &&
tsconfigResult.config.files.length > 0
? tsconfigResult.config.files.map(file =>
path.normalize(
path.resolve(path.dirname(tsconfigResult!.path), file),
),
)
: []),
// https://www.typescriptlang.org/tsconfig/#include
...(tsconfigResult.config.include !== undefined &&
tsconfigResult.config.include.length > 0
? globSync(tsconfigResult.config.include, {
ignore: [
...(tsconfigResult.config.exclude ?? []),
...defaultIgnore,
],
absolute: true,
})
: []),
]

if (files.length === 0) {
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined
}

return {
files: new Set(files.map(toNativePathSeparator)),
mapperFn,
}
})
.filter(isDefined)

mappersCachedOptions = options
}
Expand All @@ -427,3 +495,46 @@ function mangleScopedPackage(moduleName: string) {
}
return moduleName
}

/**
* Replace path `p` from `from` to `to` separator.
*
* @param {string} p Path
* @param {typeof path.sep} from From separator
* @param {typeof path.sep} to To separator
* @returns Path with `to` separator
*/
function replacePathSeparator(
p: string,
from: typeof path.sep,
to: typeof path.sep,
) {
return from === to ? p : p.replaceAll(from, to)
}

/**
* Replace path `p` separator to its native separator.
*
* @param {string} p Path
* @returns Path with native separator
*/
function toNativePathSeparator(p: string) {
return replacePathSeparator(
p,
path[process.platform === 'win32' ? 'posix' : 'win32'].sep,
path[process.platform === 'win32' ? 'win32' : 'posix'].sep,
)
}

/**
* Check if value is defined.
*
* Helper function for TypeScript.
* Should be removed when upgrading to TypeScript >= 5.5.
*
* @param {T | null | undefined} value Value
* @returns `true` if value is defined, `false` otherwise
*/
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
2 changes: 1 addition & 1 deletion tests/withJsExtension/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"#/*": ["*"]
}
},
"includes": ["./**/*"]
"include": ["./**/*"]
}

0 comments on commit fcc8883

Please sign in to comment.