diff --git a/.changeset/long-insects-tan.md b/.changeset/long-insects-tan.md new file mode 100644 index 0000000..4f341cd --- /dev/null +++ b/.changeset/long-insects-tan.md @@ -0,0 +1,5 @@ +--- +'eslint-import-resolver-typescript': patch +--- + +fix: if file has no corresponding mapper function, apply all of them, starting with the nearest one. diff --git a/.size-limit.json b/.size-limit.json index 0c0657a..648747e 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,6 +1,6 @@ [ { "path": "./lib/index.js", - "limit": "3kB" + "limit": "3.1kB" } ] diff --git a/package.json b/package.json index c35a251..cf2511e 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "test:importXResolverV3": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint --config=tests/importXResolverV3/eslint.config.js tests/importXResolverV3", "test:multipleEslintrcs": "eslint --ext ts,tsx tests/multipleEslintrcs", "test:multipleTsconfigs": "eslint --ext ts,tsx tests/multipleTsconfigs", + "test:nearestTsconfig": "eslint --ext ts,tsx tests/nearestTsconfig", "test:withJsExtension": "node tests/withJsExtension/test.js && eslint --ext ts,tsx tests/withJsExtension", "test:withJsconfig": "eslint --ext js tests/withJsconfig", "test:withPaths": "eslint --ext ts,tsx tests/withPaths", diff --git a/src/index.ts b/src/index.ts index 1287aef..b774b3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,6 +112,7 @@ let prevCwd: string let mappersCachedOptions: InternalResolverOptions let mappers: Array<{ + path: string files: Set mapperFn: NonNullable> }> = [] @@ -311,35 +312,50 @@ function getMappedPaths( paths = [resolved] } } else { - 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) + // Filter mapper functions associated with file + let mapperFns: Array>> = + mappers + .filter(({ files }) => files.has(file)) + .map(({ mapperFn }) => mapperFn) + if (mapperFns.length === 0) { + // If empty, try all mapper functions, starting with the nearest one + mapperFns = mappers + .map(mapper => ({ + mapperFn: mapper.mapperFn, + counter: equalChars(path.dirname(file), path.dirname(mapper.path)), + })) + .sort( + (a, b) => + // Sort in descending order where the nearest one has the longest counter + b.counter - a.counter, + ) + .map(({ mapperFn }) => mapperFn) + } + paths = mapperFns + .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 } - } catch { - return false - } - return false - }) + return false + }) } if (retry && paths.length === 0) { @@ -487,6 +503,7 @@ function initMappers(options: InternalResolverOptions) { } return { + path: toNativePathSeparator(tsconfigResult.path), files: new Set(files.map(toNativePathSeparator)), mapperFn, } @@ -551,3 +568,23 @@ function toNativePathSeparator(p: string) { function isDefined(value: T | null | undefined): value is T { return value !== null && value !== undefined } + +/** + * Counts how many characters in strings `a` and `b` are exactly the same and in the same position. + * + * @param {string} a First string + * @param {string} b Second string + * @returns Number of matching characters + */ +function equalChars(a: string, b: string): number { + if (a.length === 0 || b.length === 0) { + return 0 + } + + let i = 0 + const length = Math.min(a.length, b.length) + while (i < length && a.charAt(i) === b.charAt(i)) { + i += 1 + } + return i +} diff --git a/tests/nearestTsconfig/.eslintrc.cjs b/tests/nearestTsconfig/.eslintrc.cjs new file mode 100644 index 0000000..285ee8d --- /dev/null +++ b/tests/nearestTsconfig/.eslintrc.cjs @@ -0,0 +1,10 @@ +const path = require('node:path') + +const project = [ + 'tsconfig.json', + 'a/tsconfig.json', + 'a/b/tsconfig.json', + 'a/b/c/tsconfig.json', +].map(tsconfig => path.resolve(__dirname, tsconfig)) + +module.exports = require('../baseEslintConfig.cjs')(project) diff --git a/tests/nearestTsconfig/a/app/app.ts b/tests/nearestTsconfig/a/app/app.ts new file mode 100644 index 0000000..9597f5b --- /dev/null +++ b/tests/nearestTsconfig/a/app/app.ts @@ -0,0 +1,2 @@ +import 'components/a' +import 'components/root' diff --git a/tests/nearestTsconfig/a/b/app/app.ts b/tests/nearestTsconfig/a/b/app/app.ts new file mode 100644 index 0000000..61b335e --- /dev/null +++ b/tests/nearestTsconfig/a/b/app/app.ts @@ -0,0 +1,2 @@ +import 'components/b' +import 'components/root' diff --git a/tests/nearestTsconfig/a/b/c/app/app.ts b/tests/nearestTsconfig/a/b/c/app/app.ts new file mode 100644 index 0000000..e49cf01 --- /dev/null +++ b/tests/nearestTsconfig/a/b/c/app/app.ts @@ -0,0 +1,2 @@ +import 'components/c' +import 'components/root' diff --git a/tests/nearestTsconfig/a/b/c/components/c.ts b/tests/nearestTsconfig/a/b/c/components/c.ts new file mode 100644 index 0000000..da4b0ed --- /dev/null +++ b/tests/nearestTsconfig/a/b/c/components/c.ts @@ -0,0 +1 @@ +export default 'c' diff --git a/tests/nearestTsconfig/a/b/c/tsconfig.json b/tests/nearestTsconfig/a/b/c/tsconfig.json new file mode 100644 index 0000000..6f4b6a2 --- /dev/null +++ b/tests/nearestTsconfig/a/b/c/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "components/*": ["../../../components/*", "./components/*"] + } + }, + "files": ["components/c.ts"] +} diff --git a/tests/nearestTsconfig/a/b/components/b.ts b/tests/nearestTsconfig/a/b/components/b.ts new file mode 100644 index 0000000..a3bb490 --- /dev/null +++ b/tests/nearestTsconfig/a/b/components/b.ts @@ -0,0 +1 @@ +export default 'b' diff --git a/tests/nearestTsconfig/a/b/tsconfig.json b/tests/nearestTsconfig/a/b/tsconfig.json new file mode 100644 index 0000000..fedccdd --- /dev/null +++ b/tests/nearestTsconfig/a/b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "components/*": ["../../components/*", "./components/*"] + } + }, + "files": ["components/b.ts"] +} diff --git a/tests/nearestTsconfig/a/components/a.ts b/tests/nearestTsconfig/a/components/a.ts new file mode 100644 index 0000000..90bd54c --- /dev/null +++ b/tests/nearestTsconfig/a/components/a.ts @@ -0,0 +1 @@ +export default 'a' diff --git a/tests/nearestTsconfig/a/tsconfig.json b/tests/nearestTsconfig/a/tsconfig.json new file mode 100644 index 0000000..c01728f --- /dev/null +++ b/tests/nearestTsconfig/a/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "components/*": ["../components/*", "./components/*"] + } + }, + "files": ["components/a.ts"] +} diff --git a/tests/nearestTsconfig/app/app.ts b/tests/nearestTsconfig/app/app.ts new file mode 100644 index 0000000..60f1cde --- /dev/null +++ b/tests/nearestTsconfig/app/app.ts @@ -0,0 +1 @@ +import 'components/root' diff --git a/tests/nearestTsconfig/components/root.ts b/tests/nearestTsconfig/components/root.ts new file mode 100644 index 0000000..8c841b3 --- /dev/null +++ b/tests/nearestTsconfig/components/root.ts @@ -0,0 +1 @@ +export default 'root' diff --git a/tests/nearestTsconfig/tsconfig.json b/tests/nearestTsconfig/tsconfig.json new file mode 100644 index 0000000..21cf578 --- /dev/null +++ b/tests/nearestTsconfig/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "components/*": ["./components/*"] + } + }, + "files": ["components/root.ts"] +}