diff --git a/.changeset/cuddly-kiwis-care.md b/.changeset/cuddly-kiwis-care.md new file mode 100644 index 0000000..e64d340 --- /dev/null +++ b/.changeset/cuddly-kiwis-care.md @@ -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. diff --git a/.size-limit.json b/.size-limit.json index 85cbdb5..0c0657a 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,6 +1,6 @@ [ { "path": "./lib/index.js", - "limit": "2.8kB" + "limit": "3kB" } ] diff --git a/src/index.ts b/src/index.ts index 6473283..226d6ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 + mapperFn: NonNullable> +}> = [] let resolverCachedOptions: InternalResolverOptions let cachedResolver: Resolver | undefined @@ -159,7 +162,7 @@ export function resolve( resolver = cachedResolver } - log('looking for:', source) + log('looking for', source, 'in', file) source = removeQuerystring(source) @@ -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) { @@ -367,9 +371,10 @@ 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() ) { @@ -377,40 +382,103 @@ function initMappers(options: InternalResolverOptions) { } 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 } @@ -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(value: T | null | undefined): value is T { + return value !== null && value !== undefined +} diff --git a/tests/withJsExtension/tsconfig.json b/tests/withJsExtension/tsconfig.json index 13d6d9f..0cec9f2 100644 --- a/tests/withJsExtension/tsconfig.json +++ b/tests/withJsExtension/tsconfig.json @@ -6,5 +6,5 @@ "#/*": ["*"] } }, - "includes": ["./**/*"] + "include": ["./**/*"] }