From c97fb780b415a92d2657fe05a983546161d84d1d Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 13 Mar 2025 01:04:41 +0800 Subject: [PATCH 1/7] feat: rewrite, speed up by using `oxc-resolver` under the hood --- eslint.config.js | 7 + package.json | 8 +- src/constants.ts | 75 +++++ src/helpers.ts | 68 ++++ src/index.ts | 677 ++++++++++----------------------------- src/logger.ts | 5 + src/normalize-options.ts | 93 ++++++ src/types.ts | 8 + yarn.lock | 14 +- 9 files changed, 429 insertions(+), 526 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/helpers.ts create mode 100644 src/logger.ts create mode 100644 src/normalize-options.ts create mode 100644 src/types.ts diff --git a/eslint.config.js b/eslint.config.js index 0685be0..6faeac9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,6 +19,13 @@ const config = [ }, }, }, + { + files: ['src/*'], + rules: { + 'prefer-const': ['error', { destructuring: 'all' }], + 'sonarjs/no-nested-assignment': 'off', + }, + }, ] export default config diff --git a/package.json b/package.json index ed96051..9402f8e 100644 --- a/package.json +++ b/package.json @@ -12,22 +12,18 @@ "license": "ISC", "packageManager": "yarn@4.7.0", "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^16.17.0 || >=18.6.0" }, "main": "lib/index.cjs", "module": "lib/index.js", "exports": { ".": { "types": "./lib/index.d.ts", - "es2020": "./lib/index.es2020.mjs", - "fesm2020": "./lib/index.es2020.mjs", "import": "./lib/index.js", "require": "./lib/index.cjs" }, "./package.json": "./package.json" }, - "es2020": "lib/index.es2020.mjs", - "fesm2020": "lib/index.es2020.mjs", "types": "lib/index.d.ts", "files": [ "lib", @@ -42,7 +38,7 @@ "plugin" ], "scripts": { - "build": "run-p 'build:*'", + "build": "run-p -c 'build:*'", "build:r": "r -f cjs,es2020", "build:ts": "tsc -b", "eslint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --cache", diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..25a218f --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,75 @@ +export const defaultConditionNames = [ + 'types', + 'import', + + // APF: https://angular.io/guide/angular-package-format + 'esm2020', + 'es2020', + 'es2015', + + 'require', + 'node', + 'node-addons', + 'browser', + 'default', +] + +/** + * `.mts`, `.cts`, `.d.mts`, `.d.cts`, `.mjs`, `.cjs` are not included because `.cjs` and `.mjs` must be used explicitly + */ +export const defaultExtensions = [ + '.ts', + '.tsx', + '.d.ts', + '.js', + '.jsx', + '.json', + '.node', +] + +export const defaultExtensionAlias = { + '.js': [ + '.ts', + // `.tsx` can also be compiled as `.js` + '.tsx', + '.d.ts', + '.js', + ], + '.jsx': ['.tsx', '.d.ts', '.jsx'], + '.cjs': ['.cts', '.d.cts', '.cjs'], + '.mjs': ['.mts', '.d.mts', '.mjs'], +} + +export const defaultMainFields = [ + 'types', + 'typings', + + // APF: https://angular.io/guide/angular-package-format + 'fesm2020', + 'fesm2015', + 'esm2020', + 'es2020', + + 'module', + 'jsnext:main', + + 'main', +] + +export const JS_EXT_PATTERN = /\.(?:[cm]js|jsx?)$/ + +export const IMPORT_RESOLVER_NAME = 'eslint-import-resolver-typescript' + +export const interfaceVersion = 2 + +export const DEFAULT_TSCONFIG = 'tsconfig.json' + +export const DEFAULT_JSCONFIG = 'jsconfig.json' + +export const DEFAULT_CONFIGS = [DEFAULT_TSCONFIG, DEFAULT_JSCONFIG] + +export const DEFAULT_TRY_PATHS = ['', ...DEFAULT_CONFIGS] + +export const MATCH_ALL = '**' + +export const DEFAULT_IGNORE = [MATCH_ALL, 'node_modules', MATCH_ALL].join('/') diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..9e83cdd --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,68 @@ +import fs from 'node:fs' +import path from 'node:path' + +/** + * For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. + */ +export function mangleScopedPackage(moduleName: string) { + if (moduleName.startsWith('@')) { + const replaceSlash = moduleName.replace('/', '__') + if (replaceSlash !== moduleName) { + return replaceSlash.slice(1) // Take off the "@" + } + } + return moduleName +} + +/** Remove any trailing querystring from module id. */ +export function removeQuerystring(id: string) { + const querystringIndex = id.lastIndexOf('?') + if (querystringIndex !== -1) { + return id.slice(0, querystringIndex) + } + return id +} + +export const tryFile = ( + filename?: string[] | string, + includeDir = false, + base = process.cwd(), +): string => { + if (typeof filename === 'string') { + const filepath = path.resolve(base, filename) + return fs.existsSync(filepath) && + (includeDir || fs.statSync(filepath).isFile()) + ? filepath + : '' + } + + for (const file of filename ?? []) { + const filepath = tryFile(file, includeDir, base) + if (filepath) { + return filepath + } + } + + return '' +} + +const computeAffinity = (projectDir: string, targetDir: string): number => { + const a = projectDir.split(path.sep) + const b = targetDir.split(path.sep) + let lca = 0 + while (lca < a.length && lca < b.length && a[lca] === b[lca]) { + lca++ + } + return a.length - lca + (b.length - lca) +} + +export const sortProjectsByAffinity = (projects: string[], file: string) => { + const fileDir = path.dirname(file) + return projects + .map(project => ({ + project, + affinity: computeAffinity(path.dirname(project), fileDir), + })) + .sort((a, b) => a.affinity - b.affinity) + .map(item => item.project) +} diff --git a/src/index.ts b/src/index.ts index 8efc987..9c0dff8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,157 +1,73 @@ -import fs from 'node:fs' -import module from 'node:module' +import { isBuiltin } from 'node:module' import path from 'node:path' -import isNodeCoreModule from '@nolyfill/is-core-module' -import debug from 'debug' -import type { TsConfigResult } from 'get-tsconfig' -import { createPathsMatcher, getTsconfig } from 'get-tsconfig' -import type { Version } from 'is-bun-module' -import { isBunModule } from 'is-bun-module' -import { type NapiResolveOptions, ResolverFactory } from 'rspack-resolver' -import { stableHash } from 'stable-hash' -import { globSync, isDynamicPattern } from 'tinyglobby' -import type { SetRequired } from 'type-fest' - -const IMPORTER_NAME = 'eslint-import-resolver-typescript' - -const log = debug(IMPORTER_NAME) - -export const defaultConditionNames = [ - 'types', - 'import', - - // APF: https://angular.io/guide/angular-package-format - 'esm2020', - 'es2020', - 'es2015', - - 'require', - 'node', - 'node-addons', - 'browser', - 'default', -] - -/** - * `.mts`, `.cts`, `.d.mts`, `.d.cts`, `.mjs`, `.cjs` are not included because `.cjs` and `.mjs` must be used explicitly - */ -export const defaultExtensions = [ - '.ts', - '.tsx', - '.d.ts', - '.js', - '.jsx', - '.json', - '.node', -] - -export const defaultExtensionAlias = { - '.js': [ - '.ts', - // `.tsx` can also be compiled as `.js` - '.tsx', - '.d.ts', - '.js', - ], - '.jsx': ['.tsx', '.d.ts', '.jsx'], - '.cjs': ['.cts', '.d.cts', '.cjs'], - '.mjs': ['.mts', '.d.mts', '.mjs'], -} - -export const defaultMainFields = [ - 'types', - 'typings', - - // APF: https://angular.io/guide/angular-package-format - 'fesm2020', - 'fesm2015', - 'esm2020', - 'es2020', - - 'module', - 'jsnext:main', - - 'main', -] - -export const interfaceVersion = 2 - -export interface TsResolverOptions extends NapiResolveOptions { - alwaysTryTypes?: boolean - project?: string[] | string -} - -type InternalResolverOptions = SetRequired< - NapiResolveOptions, - 'conditionNames' | 'extensionAlias' | 'extensions' | 'mainFields' -> & - TsResolverOptions - -const JS_EXT_PATTERN = /\.(?:[cm]js|jsx?)$/ -const RELATIVE_PATH_PATTERN = /^\.{1,2}(?:\/.*)?$/ - -let previousOptionsHash: string -let optionsHash: string -let cachedOptions: InternalResolverOptions | undefined - -let cachedCwd: string - -let mappersCachedOptions: InternalResolverOptions -let mappers: Array<{ - path: string - files: Set - mapperFn: NonNullable> -}> = [] - -let resolverCachedOptions: InternalResolverOptions -let cachedResolver: ResolverFactory | undefined - -/** - * @param source the module to resolve; i.e './some-module' - * @param file the importing file's full path; i.e. '/usr/local/bin/file.js' - * @param options - */ -// eslint-disable-next-line sonarjs/cognitive-complexity -export function resolve( +import type { ResolvedResult } from 'eslint-plugin-import-x/types.js' +import { + type FileMatcher, + type TsConfigJsonResolved, + createFilesMatcher, + parseTsconfig, +} from 'get-tsconfig' +import { type Version, isBunModule } from 'is-bun-module' +import { ResolverFactory } from 'rspack-resolver' +import stableHash_ from 'stable-hash' + +import { JS_EXT_PATTERN } from './constants.js' +import { + mangleScopedPackage, + removeQuerystring, + sortProjectsByAffinity, +} from './helpers.js' +import { log } from './logger.js' +import { normalizeOptions } from './normalize-options.js' +import type { TypeScriptResolverOptions } from './types.js' + +export * from './constants.js' +export * from './helpers.js' +export * from './normalize-options.js' +export type * from './types.js' + +// CJS <-> ESM interop +const stableHash = 'default' in stableHash_ ? stableHash_.default : stableHash_ + +const resolverCache = new Map() + +const tsconfigCache = new Map() + +const matcherCache = new Map() + +const oxcResolve = ( source: string, file: string, - options?: TsResolverOptions | null, - resolver?: ResolverFactory | null, -): { - found: boolean - path?: string | null -} { - if ( - !cachedOptions || - previousOptionsHash !== (optionsHash = stableHash(options)) - ) { - previousOptionsHash = optionsHash - cachedOptions = { - ...options, - conditionNames: options?.conditionNames ?? defaultConditionNames, - extensions: options?.extensions ?? defaultExtensions, - extensionAlias: options?.extensionAlias ?? defaultExtensionAlias, - mainFields: options?.mainFields ?? defaultMainFields, + resolver: ResolverFactory, +): ResolvedResult => { + const result = resolver.sync(path.dirname(file), source) + if (result.path) { + return { + found: true, + path: result.path, } } - - if (!resolver) { - if (!cachedResolver || resolverCachedOptions !== cachedOptions) { - cachedResolver = new ResolverFactory(cachedOptions) - resolverCachedOptions = cachedOptions - } - resolver = cachedResolver + if (result.error) { + log('oxc resolve error:', result.error) } + return { + found: false, + } +} - log('looking for', source, 'in', file) - - source = removeQuerystring(source) - +export const resolve = ( + source: string, + file: string, + options?: TypeScriptResolverOptions, + resolver?: ResolverFactory | null, + // eslint-disable-next-line sonarjs/cognitive-complexity +): ResolvedResult => { // don't worry about core node/bun modules if ( - isNodeCoreModule(source) || - isBunModule(source, (process.versions.bun ?? 'latest') as Version) + isBuiltin(source) || + (process.versions.bun && + isBunModule(source, process.versions.bun as Version)) ) { log('matched core:', source) @@ -161,407 +77,142 @@ export function resolve( } } - /** - * {@link https://github.com/webpack/enhanced-resolve/blob/38e9fd9acb79643a70e7bcd0d85dabc600ea321f/lib/PnpPlugin.js#L81-L83} - */ - if (process.versions.pnp && source === 'pnpapi') { - return { - found: true, - path: module.findPnpApi(file).resolveToUnqualified(source, file, { - considerBuiltins: false, - }), + options ||= {} + + if (!resolver) { + const optionsHash = stableHash(options) + options = normalizeOptions(options) + // take `cwd` into account -- #217 + const cacheKey = `${optionsHash}:${process.cwd()}` + let cached = resolverCache.get(cacheKey) + if (!cached && !options.project) { + resolverCache.set(cacheKey, (cached = new ResolverFactory(options))) } + resolver = cached } - initMappers(cachedOptions) - - let mappedPaths = getMappedPaths(source, file, cachedOptions.extensions, true) + source = removeQuerystring(source) - if (mappedPaths.length > 0) { - log('matched ts path:', ...mappedPaths) - } else { - mappedPaths = [source] - } + // eslint-disable-next-line sonarjs/label-position, sonarjs/no-labels + resolve: if (!resolver) { + // must be a array with 2+ items here already ensured by `normalizeOptions` + const project = options.project as string[] + for (const tsconfigPath of project) { + const resolverCached = resolverCache.get(tsconfigPath) + if (resolverCached) { + resolver = resolverCached + break resolve + } + let tsconfigCached = tsconfigCache.get(tsconfigPath) + if (!tsconfigCached) { + tsconfigCache.set( + tsconfigPath, + (tsconfigCached = parseTsconfig(tsconfigPath)), + ) + } + let matcherCached = matcherCache.get(tsconfigPath) + if (!matcherCached) { + matcherCache.set( + tsconfigPath, + (matcherCached = createFilesMatcher({ + config: tsconfigCached, + path: tsconfigPath, + })), + ) + } + const tsconfig = matcherCached(file) + if (!tsconfig) { + log('tsconfig', tsconfigPath, 'does not match', file) + continue + } + log('matched tsconfig at:', tsconfigPath, 'for', file) + options = { + ...options, + tsconfig: { + references: 'auto', + ...options.tsconfig, + configFile: tsconfigPath, + }, + } + resolver = new ResolverFactory(options) + resolverCache.set(tsconfigPath, resolver) + break resolve + } - // note that even if we map the path, we still need to do a final resolve - let foundNodePath: string | undefined - for (const mappedPath of mappedPaths) { - try { - const resolved = resolver.sync( - path.dirname(path.resolve(file)), - mappedPath, + log( + 'no tsconfig matched', + file, + 'with', + ...project, + ', trying from the the nearest one', + ) + for (const p of sortProjectsByAffinity(project, file)) { + const resolved = resolve( + source, + file, + { ...options, project: p }, + resolver, ) - if (resolved.path) { - foundNodePath = resolved.path - break + if (resolved.found) { + return resolved } - } catch { - log('failed to resolve with', mappedPath) } } + if (!resolver) { + return { + found: false, + } + } + + const resolved = oxcResolve(source, file, resolver) + + const foundPath = resolved.path + // naive attempt at `@types/*` resolution, // if path is neither absolute nor relative if ( - (JS_EXT_PATTERN.test(foundNodePath!) || - (cachedOptions.alwaysTryTypes && !foundNodePath)) && + ((foundPath && JS_EXT_PATTERN.test(foundPath)) || + (options?.alwaysTryTypes && !foundPath)) && !/^@types[/\\]/.test(source) && !path.isAbsolute(source) && !source.startsWith('.') ) { - const definitelyTyped = resolve( - '@types' + path.sep + mangleScopedPackage(source), + const definitelyTyped = oxcResolve( + '@types/' + mangleScopedPackage(source), file, - options, + resolver, ) + if (definitelyTyped.found) { return definitelyTyped } } - if (foundNodePath) { - log('matched node path:', foundNodePath) - - return { - found: true, - path: foundNodePath, - } + if (foundPath) { + log('matched path:', foundPath) + } else { + log( + "didn't find", + source, + 'with', + options.tsconfig?.configFile || options.project, + ) } - log("didn't find ", source) - - return { - found: false, - } + return resolved } -export function createTypeScriptImportResolver( - options?: TsResolverOptions | null, -) { - const resolver = new ResolverFactory({ - ...options, - conditionNames: options?.conditionNames ?? defaultConditionNames, - extensions: options?.extensions ?? defaultExtensions, - extensionAlias: options?.extensionAlias ?? defaultExtensionAlias, - mainFields: options?.mainFields ?? defaultMainFields, - }) - +export const createTypeScriptImportResolver = ( + options?: TypeScriptResolverOptions, +) => { + options = normalizeOptions(options) + const resolver = options.project ? null : new ResolverFactory(options) return { interfaceVersion: 3, - name: IMPORTER_NAME, + name: 'eslint-import-resolver-typescript', resolve(source: string, file: string) { return resolve(source, file, options, resolver) }, } } - -/** Remove any trailing querystring from module id. */ -function removeQuerystring(id: string) { - const querystringIndex = id.lastIndexOf('?') - if (querystringIndex !== -1) { - return id.slice(0, querystringIndex) - } - return id -} - -const isFile = (path?: string): path is string => { - try { - return !!(path && fs.statSync(path, { throwIfNoEntry: false })?.isFile()) - } catch { - // Node 12 does not support throwIfNoEntry. - return false - } -} - -const isModule = (modulePath?: string): modulePath is string => - !!modulePath && isFile(path.resolve(modulePath, 'package.json')) - -/** - * @param {string} source the module to resolve; i.e './some-module' - * @param {string} file the importing file's full path; i.e. '/usr/local/bin/file.js' - * @param {string[]} extensions the extensions to try - * @param {boolean} retry should retry on failed to resolve - * @returns The mapped path of the module or undefined - */ -// eslint-disable-next-line sonarjs/cognitive-complexity -function getMappedPaths( - source: string, - file: string, - extensions: string[] = defaultExtensions, - retry?: boolean, -): string[] { - const originalExtensions = extensions - extensions = ['', ...extensions] - - let paths: string[] = [] - - if (RELATIVE_PATH_PATTERN.test(source)) { - const resolved = path.resolve(path.dirname(file), source) - if (isFile(resolved)) { - paths = [resolved] - } - } else { - // 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(/* The depth is always 2 */ 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) { - const isJs = JS_EXT_PATTERN.test(source) - if (isJs) { - const jsExt = path.extname(source) - // cjs -> cts, js -> ts, jsx -> tsx, mjs -> mts - const tsExt = jsExt.replace('js', 'ts') - - const basename = source.replace(JS_EXT_PATTERN, '') - - let resolved = getMappedPaths(basename + tsExt, file) - - if (resolved.length === 0 && jsExt === '.js') { - // js -> tsx - const tsxExt = jsExt.replace('js', 'tsx') - resolved = getMappedPaths(basename + tsxExt, file) - } - - if (resolved.length === 0) { - resolved = getMappedPaths( - basename + '.d' + (tsExt === '.tsx' ? '.ts' : tsExt), - file, - ) - } - - if (resolved.length > 0) { - return resolved - } - } - - for (const ext of extensions) { - const mappedPaths = isJs ? [] : getMappedPaths(source + ext, file) - const resolved = - mappedPaths.length > 0 - ? mappedPaths - : getMappedPaths(source + `/index${ext}`, file) - - if (resolved.length > 0) { - return resolved - } - } - } - - return paths -} - -function initMappers(options: InternalResolverOptions) { - if ( - mappers.length > 0 && - mappersCachedOptions === options && - cachedCwd === process.cwd() - ) { - return - } - cachedCwd = process.cwd() - const configPaths = ( - typeof options.project === 'string' - ? [options.project] - : // eslint-disable-next-line sonarjs/no-nested-conditional - Array.isArray(options.project) - ? options.project - : [cachedCwd] - ) // 'tinyglobby' pattern must have POSIX separator - .map(config => replacePathSeparator(config, path.sep, path.posix.sep)) - - // https://github.com/microsoft/TypeScript/blob/df342b7206cb56b56bb3b3aecbb2ee2d2ff7b217/src/compiler/commandLineParser.ts#L3006 - const defaultInclude = ['**/*'] - const defaultIgnore = ['**/node_modules/**'] - - // Turn glob patterns into paths - const projectPaths = [ - ...new Set([ - ...configPaths - .filter(p => !isDynamicPattern(p)) - .map(p => path.resolve(process.cwd(), p)), - ...globSync( - configPaths.filter(path => isDynamicPattern(path)), - { - absolute: true, - dot: true, - expandDirectories: false, - ignore: defaultIgnore, - }, - ), - ]), - ] - - 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 (!tsconfigResult) { - return - } - - const mapperFn = createPathsMatcher(tsconfigResult) - - if (!mapperFn) { - return - } - - const files = - tsconfigResult.config.files == null && - tsconfigResult.config.include == null - ? // Include everything if no files or include options - globSync(defaultInclude, { - absolute: true, - cwd: path.dirname(tsconfigResult.path), - dot: true, - ignore: [ - ...(tsconfigResult.config.exclude ?? []), - ...defaultIgnore, - ], - }) - : [ - // https://www.typescriptlang.org/tsconfig/#files - ...(tsconfigResult.config.files != null && - 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 != null && - tsconfigResult.config.include.length > 0 - ? globSync(tsconfigResult.config.include, { - absolute: true, - cwd: path.dirname(tsconfigResult.path), - dot: true, - ignore: [ - ...(tsconfigResult.config.exclude ?? []), - ...defaultIgnore, - ], - }) - : []), - ] - - return { - path: toNativePathSeparator(tsconfigResult.path), - files: new Set(files.map(toNativePathSeparator)), - mapperFn, - } - }) - .filter(Boolean) - - mappersCachedOptions = options -} - -/** - * For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. - */ -function mangleScopedPackage(moduleName: string) { - if (moduleName.startsWith('@')) { - const replaceSlash = moduleName.replace(path.sep, '__') - if (replaceSlash !== moduleName) { - return replaceSlash.slice(1) // Take off the "@" - } - } - 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, - ) -} - -/** - * 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/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..cc9ecb5 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,5 @@ +import debug from 'debug' + +import { IMPORT_RESOLVER_NAME } from './constants.js' + +export const log = debug(IMPORT_RESOLVER_NAME) diff --git a/src/normalize-options.ts b/src/normalize-options.ts new file mode 100644 index 0000000..f1d80c2 --- /dev/null +++ b/src/normalize-options.ts @@ -0,0 +1,93 @@ +import type { TsconfigOptions } from 'oxc-resolver' +import { globSync, isDynamicPattern } from 'tinyglobby' + +import { + defaultConditionNames, + defaultExtensions, + defaultExtensionAlias, + defaultMainFields, + DEFAULT_CONFIGS, + DEFAULT_TRY_PATHS, + DEFAULT_IGNORE, +} from './constants.js' +import { tryFile } from './helpers.js' +import { log } from './logger.js' +import type { TsResolverOptions, TypeScriptResolverOptions } from './types.js' + +export let defaultConfigFile: string + +const configFileMapping = new Map() + +export function normalizeOptions( + options?: TypeScriptResolverOptions | null, +): TsResolverOptions +// eslint-disable-next-line sonarjs/cognitive-complexity +export function normalizeOptions( + options?: TsResolverOptions | null, +): TsResolverOptions { + let { project, tsconfig } = (options ??= {}) + + let { configFile, references }: Partial = tsconfig ?? {} + + let ensured: boolean | undefined + + if (configFile) { + configFile = tryFile(configFile) + ensured = true + } else if (project) { + project = Array.isArray(project) ? project : [project] + if (project.some(p => isDynamicPattern(p))) { + project = globSync(project, { + absolute: true, + dot: true, + onlyFiles: false, + ignore: DEFAULT_IGNORE, + }) + } + project = project.flatMap(p => tryFile(DEFAULT_TRY_PATHS, false, p) || []) + log('resolved projects:', ...project) + if (project.length === 1) { + configFile = project[0] + ensured = true + } + if (project.length <= 1) { + project = undefined + } + } + + if (!project && !configFile) { + configFile = defaultConfigFile ||= tryFile(DEFAULT_CONFIGS) + ensured = true + } + + if (configFile) { + const cachedOptions = configFileMapping.get(configFile) + if (cachedOptions) { + log('using cached options for', configFile) + return cachedOptions + } + } + + if (!ensured && configFile && configFile !== defaultConfigFile) { + configFile = tryFile(DEFAULT_TRY_PATHS, false, configFile) + } + + options = { + conditionNames: defaultConditionNames, + extensions: defaultExtensions, + extensionAlias: defaultExtensionAlias, + mainFields: defaultMainFields, + ...options, + project, + tsconfig: { + references: references ?? 'auto', + configFile: configFile || '', + }, + } + + if (configFile) { + configFileMapping.set(configFile, options) + } + + return options +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d7132c2 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,8 @@ +import type { NapiResolveOptions } from 'rspack-resolver' + +export interface TsResolverOptions extends NapiResolveOptions { + project?: string[] | string + alwaysTryTypes?: boolean +} + +export type TypeScriptResolverOptions = TsResolverOptions | null diff --git a/yarn.lock b/yarn.lock index 5217fbc..c1e0d8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,12 +219,12 @@ __metadata: linkType: hard "@ampproject/remapping@npm:^2.2.0": - version: 2.3.0 - resolution: "@ampproject/remapping@npm:2.3.0" + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10/f3451525379c68a73eb0a1e65247fbf28c0cccd126d93af21c75fceff77773d43c0d4a2d51978fb131aff25b5f2cb41a9fe48cc296e61ae65e679c4f6918b0ab + "@jridgewell/gen-mapping": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/e15fecbf3b54c988c8b4fdea8ef514ab482537e8a080b2978cc4b47ccca7140577ca7b65ad3322dcce65bc73ee6e5b90cbfe0bbd8c766dad04d5c62ec9634c42 languageName: node linkType: hard @@ -2829,7 +2829,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.8 resolution: "@jridgewell/gen-mapping@npm:0.3.8" dependencies: @@ -2871,7 +2871,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: From 3d8e5f85fbacd9632dec625cf30e429aef8339b3 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 13 Mar 2025 01:25:02 +0800 Subject: [PATCH 2/7] feat: enable `alwaysTryTypes` by default, add `noWarnOnMultipleProjects` option --- src/index.ts | 6 +++--- src/normalize-options.ts | 25 +++++++++++++++-------- src/types.ts | 8 +++++--- tests/base.eslintrc.cjs | 2 +- tests/importXResolverV3/eslint.config.cjs | 1 + 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9c0dff8..4a68621 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,7 +59,7 @@ const oxcResolve = ( export const resolve = ( source: string, file: string, - options?: TypeScriptResolverOptions, + options?: TypeScriptResolverOptions | null, resolver?: ResolverFactory | null, // eslint-disable-next-line sonarjs/cognitive-complexity ): ResolvedResult => { @@ -173,7 +173,7 @@ export const resolve = ( // if path is neither absolute nor relative if ( ((foundPath && JS_EXT_PATTERN.test(foundPath)) || - (options?.alwaysTryTypes && !foundPath)) && + (options.alwaysTryTypes !== false && !foundPath)) && !/^@types[/\\]/.test(source) && !path.isAbsolute(source) && !source.startsWith('.') @@ -204,7 +204,7 @@ export const resolve = ( } export const createTypeScriptImportResolver = ( - options?: TypeScriptResolverOptions, + options?: TypeScriptResolverOptions | null, ) => { options = normalizeOptions(options) const resolver = options.project ? null : new ResolverFactory(options) diff --git a/src/normalize-options.ts b/src/normalize-options.ts index f1d80c2..424c5d3 100644 --- a/src/normalize-options.ts +++ b/src/normalize-options.ts @@ -2,30 +2,32 @@ import type { TsconfigOptions } from 'oxc-resolver' import { globSync, isDynamicPattern } from 'tinyglobby' import { + DEFAULT_CONFIGS, + DEFAULT_IGNORE, + DEFAULT_TRY_PATHS, defaultConditionNames, - defaultExtensions, defaultExtensionAlias, + defaultExtensions, defaultMainFields, - DEFAULT_CONFIGS, - DEFAULT_TRY_PATHS, - DEFAULT_IGNORE, } from './constants.js' import { tryFile } from './helpers.js' import { log } from './logger.js' -import type { TsResolverOptions, TypeScriptResolverOptions } from './types.js' +import type { TypeScriptResolverOptions } from './types.js' export let defaultConfigFile: string const configFileMapping = new Map() +let warned: boolean | undefined + export function normalizeOptions( options?: TypeScriptResolverOptions | null, -): TsResolverOptions +): TypeScriptResolverOptions // eslint-disable-next-line sonarjs/cognitive-complexity export function normalizeOptions( - options?: TsResolverOptions | null, -): TsResolverOptions { - let { project, tsconfig } = (options ??= {}) + options?: TypeScriptResolverOptions | null, +): TypeScriptResolverOptions { + let { project, tsconfig, noWarnOnMultipleProjects } = (options ||= {}) let { configFile, references }: Partial = tsconfig ?? {} @@ -52,6 +54,11 @@ export function normalizeOptions( } if (project.length <= 1) { project = undefined + } else if (!warned && !noWarnOnMultipleProjects) { + warned = true + console.warn( + 'Multiple projects found, consider using a single `tsconfig` with `references` to speed up, or use `noWarnOnMultipleProjects` to suppress this warning', + ) } } diff --git a/src/types.ts b/src/types.ts index d7132c2..1043060 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,10 @@ import type { NapiResolveOptions } from 'rspack-resolver' -export interface TsResolverOptions extends NapiResolveOptions { +export interface TypeScriptResolverOptions extends NapiResolveOptions { project?: string[] | string + /** + * @default true - whether to always try to resolve `@types` packages + */ alwaysTryTypes?: boolean + noWarnOnMultipleProjects?: boolean } - -export type TypeScriptResolverOptions = TsResolverOptions | null diff --git a/tests/base.eslintrc.cjs b/tests/base.eslintrc.cjs index abab581..f0c7501 100644 --- a/tests/base.eslintrc.cjs +++ b/tests/base.eslintrc.cjs @@ -15,7 +15,7 @@ const base = project => ({ 'import-x/resolver': { typescript: { project, - alwaysTryTypes: true, + noWarnOnMultipleProjects: true, }, }, }, diff --git a/tests/importXResolverV3/eslint.config.cjs b/tests/importXResolverV3/eslint.config.cjs index 786bd4d..5eee5f0 100644 --- a/tests/importXResolverV3/eslint.config.cjs +++ b/tests/importXResolverV3/eslint.config.cjs @@ -26,6 +26,7 @@ module.exports = 'import-x/resolver-next': [ createTypeScriptImportResolver({ project: absoluteGlobPath, + noWarnOnMultipleProjects: true, }), ], }, From 02a2649cddeb082a793e0e1021d20d1050d8fa66 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 13 Mar 2025 02:20:17 +0800 Subject: [PATCH 3/7] chore: reduce size limit! --- .size-limit.json | 2 +- package.json | 1 - src/index.ts | 12 +++++++----- yarn.lock | 8 -------- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 648747e..32a6a58 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,6 +1,6 @@ [ { "path": "./lib/index.js", - "limit": "3.1kB" + "limit": "1.4kB" } ] diff --git a/package.json b/package.json index 9402f8e..c283d89 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ } }, "dependencies": { - "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^1.3.0", diff --git a/src/index.ts b/src/index.ts index 4a68621..4eb151b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { type Version, isBunModule } from 'is-bun-module' import { ResolverFactory } from 'rspack-resolver' import stableHash_ from 'stable-hash' -import { JS_EXT_PATTERN } from './constants.js' +import { IMPORT_RESOLVER_NAME, JS_EXT_PATTERN } from './constants.js' import { mangleScopedPackage, removeQuerystring, @@ -93,15 +93,17 @@ export const resolve = ( source = removeQuerystring(source) + options ||= {} + // eslint-disable-next-line sonarjs/label-position, sonarjs/no-labels - resolve: if (!resolver) { + createResolver: if (!resolver) { // must be a array with 2+ items here already ensured by `normalizeOptions` const project = options.project as string[] for (const tsconfigPath of project) { const resolverCached = resolverCache.get(tsconfigPath) if (resolverCached) { resolver = resolverCached - break resolve + break createResolver } let tsconfigCached = tsconfigCache.get(tsconfigPath) if (!tsconfigCached) { @@ -136,7 +138,7 @@ export const resolve = ( } resolver = new ResolverFactory(options) resolverCache.set(tsconfigPath, resolver) - break resolve + break createResolver } log( @@ -210,7 +212,7 @@ export const createTypeScriptImportResolver = ( const resolver = options.project ? null : new ResolverFactory(options) return { interfaceVersion: 3, - name: 'eslint-import-resolver-typescript', + name: IMPORT_RESOLVER_NAME, resolve(source: string, file: string) { return resolve(source, file, options, resolver) }, diff --git a/yarn.lock b/yarn.lock index c1e0d8f..516796e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3271,13 +3271,6 @@ __metadata: languageName: node linkType: hard -"@nolyfill/is-core-module@npm:1.0.39": - version: 1.0.39 - resolution: "@nolyfill/is-core-module@npm:1.0.39" - checksum: 10/0d6e098b871eca71d875651288e1f0fa770a63478b0b50479c99dc760c64175a56b5b04f58d5581bbcc6b552b8191ab415eada093d8df9597ab3423c8cac1815 - languageName: node - linkType: hard - "@npmcli/agent@npm:^2.0.0": version: 2.2.0 resolution: "@npmcli/agent@npm:2.2.0" @@ -6703,7 +6696,6 @@ __metadata: "@changesets/cli": "npm:^2.28.1" "@commitlint/cli": "npm:^19.8.0" "@mozilla/glean": "npm:^5.0.3" - "@nolyfill/is-core-module": "npm:1.0.39" "@pkgr/rollup": "npm:^6.0.0" "@total-typescript/ts-reset": "npm:^0.6.1" "@types/debug": "npm:^4.1.12" From 298388bd875fe3692f5e5612de35552cdb8bd75a Mon Sep 17 00:00:00 2001 From: JounQin Date: Mon, 17 Mar 2025 05:46:59 +0800 Subject: [PATCH 4/7] chore: bump rspack-resolver --- .size-limit.json | 2 +- package.json | 13 ++--- src/index.ts | 22 +++++--- src/normalize-options.ts | 2 +- yarn.lock | 118 +++++++++++++++++---------------------- 5 files changed, 73 insertions(+), 84 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 32a6a58..f8a0842 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,6 +1,6 @@ [ { "path": "./lib/index.js", - "limit": "1.4kB" + "limit": "1.5kB" } ] diff --git a/package.json b/package.json index c283d89..208b042 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,14 @@ "exports": { ".": { "types": "./lib/index.d.ts", - "import": "./lib/index.js", - "require": "./lib/index.cjs" + "require": "./lib/index.cjs", + "default": "./lib/index.js" }, "./package.json": "./package.json" }, "types": "lib/index.d.ts", "files": [ "lib", - "shim.d.ts", "!**/*.tsbuildinfo" ], "keywords": [ @@ -39,9 +38,9 @@ ], "scripts": { "build": "run-p -c 'build:*'", - "build:r": "r -f cjs,es2020", + "build:r": "r -f cjs", "build:ts": "tsc -b", - "eslint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --cache", + "eslint": "ESLINT_USE_FLAT_CONFIG=false eslint --cache", "lint": "run-p 'lint:*'", "lint:es": "eslint . --cache", "lint:tsc": "tsc --noEmit", @@ -80,7 +79,7 @@ "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^1.3.0", - "rspack-resolver": "^1.1.0", + "rspack-resolver": "^1.1.2", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.12" }, @@ -97,7 +96,6 @@ "@types/pnpapi": "^0.0.5", "@types/unist": "^3.0.3", "clean-pkg-json": "^1.2.1", - "cross-env": "^7.0.3", "dummy.js": "link:dummy.js", "eslint": "^9.22.0", "eslint-import-resolver-typescript": "link:.", @@ -110,7 +108,6 @@ "size-limit": "^11.2.0", "size-limit-preset-node-lib": "^0.3.0", "type-coverage": "^2.29.7", - "type-fest": "^4.37.0", "typescript": "~5.8.2", "yarn-berry-deduplicate": "^6.1.1" }, diff --git a/src/index.ts b/src/index.ts index 4eb151b..2ceb3f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { isBuiltin } from 'node:module' +import module from 'node:module' import path from 'node:path' import type { ResolvedResult } from 'eslint-plugin-import-x/types.js' @@ -10,7 +10,7 @@ import { } from 'get-tsconfig' import { type Version, isBunModule } from 'is-bun-module' import { ResolverFactory } from 'rspack-resolver' -import stableHash_ from 'stable-hash' +import { stableHash } from 'stable-hash' import { IMPORT_RESOLVER_NAME, JS_EXT_PATTERN } from './constants.js' import { @@ -27,9 +27,6 @@ export * from './helpers.js' export * from './normalize-options.js' export type * from './types.js' -// CJS <-> ESM interop -const stableHash = 'default' in stableHash_ ? stableHash_.default : stableHash_ - const resolverCache = new Map() const tsconfigCache = new Map() @@ -65,7 +62,7 @@ export const resolve = ( ): ResolvedResult => { // don't worry about core node/bun modules if ( - isBuiltin(source) || + module.isBuiltin(source) || (process.versions.bun && isBunModule(source, process.versions.bun as Version)) ) { @@ -77,6 +74,17 @@ export const resolve = ( } } + if (process.versions.pnp && source === 'pnpapi') { + return { + found: true, + path: module.findPnpApi(file).resolveToUnqualified(source, file, { + considerBuiltins: false, + }), + } + } + + source = removeQuerystring(source) + options ||= {} if (!resolver) { @@ -91,8 +99,6 @@ export const resolve = ( resolver = cached } - source = removeQuerystring(source) - options ||= {} // eslint-disable-next-line sonarjs/label-position, sonarjs/no-labels diff --git a/src/normalize-options.ts b/src/normalize-options.ts index 424c5d3..2a1326c 100644 --- a/src/normalize-options.ts +++ b/src/normalize-options.ts @@ -1,4 +1,4 @@ -import type { TsconfigOptions } from 'oxc-resolver' +import type { TsconfigOptions } from 'rspack-resolver' import { globSync, isDynamicPattern } from 'tinyglobby' import { diff --git a/yarn.lock b/yarn.lock index 516796e..465cfd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4248,81 +4248,81 @@ __metadata: languageName: node linkType: hard -"@unrs/rspack-resolver-binding-darwin-arm64@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-darwin-arm64@npm:1.1.0" +"@unrs/rspack-resolver-binding-darwin-arm64@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-darwin-arm64@npm:1.1.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@unrs/rspack-resolver-binding-darwin-x64@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-darwin-x64@npm:1.1.0" +"@unrs/rspack-resolver-binding-darwin-x64@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-darwin-x64@npm:1.1.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@unrs/rspack-resolver-binding-freebsd-x64@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-freebsd-x64@npm:1.1.0" +"@unrs/rspack-resolver-binding-freebsd-x64@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-freebsd-x64@npm:1.1.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@unrs/rspack-resolver-binding-linux-arm-gnueabihf@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-linux-arm-gnueabihf@npm:1.1.0" +"@unrs/rspack-resolver-binding-linux-arm-gnueabihf@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-linux-arm-gnueabihf@npm:1.1.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@unrs/rspack-resolver-binding-linux-arm64-gnu@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-linux-arm64-gnu@npm:1.1.0" +"@unrs/rspack-resolver-binding-linux-arm64-gnu@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-linux-arm64-gnu@npm:1.1.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@unrs/rspack-resolver-binding-linux-arm64-musl@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-linux-arm64-musl@npm:1.1.0" +"@unrs/rspack-resolver-binding-linux-arm64-musl@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-linux-arm64-musl@npm:1.1.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@unrs/rspack-resolver-binding-linux-x64-gnu@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-linux-x64-gnu@npm:1.1.0" +"@unrs/rspack-resolver-binding-linux-x64-gnu@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-linux-x64-gnu@npm:1.1.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@unrs/rspack-resolver-binding-linux-x64-musl@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-linux-x64-musl@npm:1.1.0" +"@unrs/rspack-resolver-binding-linux-x64-musl@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-linux-x64-musl@npm:1.1.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@unrs/rspack-resolver-binding-wasm32-wasi@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-wasm32-wasi@npm:1.1.0" +"@unrs/rspack-resolver-binding-wasm32-wasi@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-wasm32-wasi@npm:1.1.2" dependencies: "@napi-rs/wasm-runtime": "npm:^0.2.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@unrs/rspack-resolver-binding-win32-arm64-msvc@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-win32-arm64-msvc@npm:1.1.0" +"@unrs/rspack-resolver-binding-win32-arm64-msvc@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-win32-arm64-msvc@npm:1.1.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@unrs/rspack-resolver-binding-win32-x64-msvc@npm:1.1.0": - version: 1.1.0 - resolution: "@unrs/rspack-resolver-binding-win32-x64-msvc@npm:1.1.0" +"@unrs/rspack-resolver-binding-win32-x64-msvc@npm:1.1.2": + version: 1.1.2 + resolution: "@unrs/rspack-resolver-binding-win32-x64-msvc@npm:1.1.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5886,19 +5886,7 @@ __metadata: languageName: node linkType: hard -"cross-env@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-env@npm:7.0.3" - dependencies: - cross-spawn: "npm:^7.0.1" - bin: - cross-env: src/bin/cross-env.js - cross-env-shell: src/bin/cross-env-shell.js - checksum: 10/e99911f0d31c20e990fd92d6fd001f4b01668a303221227cc5cb42ed155f086351b1b3bd2699b200e527ab13011b032801f8ce638e6f09f854bdf744095e604c - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -6703,7 +6691,6 @@ __metadata: "@types/pnpapi": "npm:^0.0.5" "@types/unist": "npm:^3.0.3" clean-pkg-json: "npm:^1.2.1" - cross-env: "npm:^7.0.3" debug: "npm:^4.4.0" dummy.js: "link:dummy.js" eslint: "npm:^9.22.0" @@ -6715,14 +6702,13 @@ __metadata: npm-run-all2: "npm:^7.0.2" prettier: "npm:^3.5.3" react: "npm:^19.0.0" - rspack-resolver: "npm:^1.1.0" + rspack-resolver: "npm:^1.1.2" simple-git-hooks: "npm:^2.11.1" size-limit: "npm:^11.2.0" size-limit-preset-node-lib: "npm:^0.3.0" stable-hash: "npm:^0.0.5" tinyglobby: "npm:^0.2.12" type-coverage: "npm:^2.29.7" - type-fest: "npm:^4.37.0" typescript: "npm:~5.8.2" yarn-berry-deduplicate: "npm:^6.1.1" peerDependencies: @@ -13221,21 +13207,21 @@ __metadata: languageName: node linkType: hard -"rspack-resolver@npm:^1.1.0": - version: 1.1.0 - resolution: "rspack-resolver@npm:1.1.0" - dependencies: - "@unrs/rspack-resolver-binding-darwin-arm64": "npm:1.1.0" - "@unrs/rspack-resolver-binding-darwin-x64": "npm:1.1.0" - "@unrs/rspack-resolver-binding-freebsd-x64": "npm:1.1.0" - "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "npm:1.1.0" - "@unrs/rspack-resolver-binding-linux-arm64-gnu": "npm:1.1.0" - "@unrs/rspack-resolver-binding-linux-arm64-musl": "npm:1.1.0" - "@unrs/rspack-resolver-binding-linux-x64-gnu": "npm:1.1.0" - "@unrs/rspack-resolver-binding-linux-x64-musl": "npm:1.1.0" - "@unrs/rspack-resolver-binding-wasm32-wasi": "npm:1.1.0" - "@unrs/rspack-resolver-binding-win32-arm64-msvc": "npm:1.1.0" - "@unrs/rspack-resolver-binding-win32-x64-msvc": "npm:1.1.0" +"rspack-resolver@npm:^1.1.0, rspack-resolver@npm:^1.1.2": + version: 1.1.2 + resolution: "rspack-resolver@npm:1.1.2" + dependencies: + "@unrs/rspack-resolver-binding-darwin-arm64": "npm:1.1.2" + "@unrs/rspack-resolver-binding-darwin-x64": "npm:1.1.2" + "@unrs/rspack-resolver-binding-freebsd-x64": "npm:1.1.2" + "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "npm:1.1.2" + "@unrs/rspack-resolver-binding-linux-arm64-gnu": "npm:1.1.2" + "@unrs/rspack-resolver-binding-linux-arm64-musl": "npm:1.1.2" + "@unrs/rspack-resolver-binding-linux-x64-gnu": "npm:1.1.2" + "@unrs/rspack-resolver-binding-linux-x64-musl": "npm:1.1.2" + "@unrs/rspack-resolver-binding-wasm32-wasi": "npm:1.1.2" + "@unrs/rspack-resolver-binding-win32-arm64-msvc": "npm:1.1.2" + "@unrs/rspack-resolver-binding-win32-x64-msvc": "npm:1.1.2" dependenciesMeta: "@unrs/rspack-resolver-binding-darwin-arm64": optional: true @@ -13259,7 +13245,7 @@ __metadata: optional: true "@unrs/rspack-resolver-binding-win32-x64-msvc": optional: true - checksum: 10/b8582e0d28596ba9ed8a4cd19b04b6e96e0e0963cdc0d5f365fc7d46335e5bbdb44ef433c2e0a560dedb3f4cd55cc13788f1d3b223f83cf4a3a7f8550b96e305 + checksum: 10/116fadb51a778560a079d82874e85d519a82f54987992fbde766c912597c347f5517cd7f8223bcc2658e742b8cd4f3beb5d994a48b0fbbe4818af75dcf994481 languageName: node linkType: hard @@ -14340,7 +14326,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:4.37.0, type-fest@npm:^4.37.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1, type-fest@npm:^4.8.2": +"type-fest@npm:4.37.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1, type-fest@npm:^4.8.2": version: 4.37.0 resolution: "type-fest@npm:4.37.0" checksum: 10/882cf05374d7c635cbbbc50cb89863dad3d27a77c426d062144ba32b23a44087193213c5bbd64f3ab8be04215005c950286567be06fecca9d09c66abd290ef01 From b83045a8767d0fad58e54c7e08f38b7af2d65d2b Mon Sep 17 00:00:00 2001 From: JounQin Date: Mon, 17 Mar 2025 05:59:26 +0800 Subject: [PATCH 5/7] Create friendly-weeks-act.md --- .changeset/friendly-weeks-act.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/friendly-weeks-act.md diff --git a/.changeset/friendly-weeks-act.md b/.changeset/friendly-weeks-act.md new file mode 100644 index 0000000..9fe7899 --- /dev/null +++ b/.changeset/friendly-weeks-act.md @@ -0,0 +1,12 @@ +--- +"eslint-import-resolver-typescript": major +--- + +feat!: rewrite, speed up by using `rspack-resolver` which supports `references` natively under the hood + +BREAKING CHANGES: + +- drop Node 14 support, Node `^16.17.0 || >=18.6` is now required +- `alwaysTryTypes` is enabled by default, you can set it as `true` to opt-out +- array type of `project` is discouraged but still supported, single `project` with `references` are encouraged for better performance, you can enable `noWarnOnMultipleProjects` option to supress the warning message +- root `tsconfig.json` or `jsconfig.json` will be used automatically if no `project` provided From d08485b8055d7ed834f4e542339c093dbe47bf41 Mon Sep 17 00:00:00 2001 From: JounQin Date: Mon, 17 Mar 2025 06:01:00 +0800 Subject: [PATCH 6/7] chore: don't run test with eslint cache --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 208b042..7ea3953 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "build": "run-p -c 'build:*'", "build:r": "r -f cjs", "build:ts": "tsc -b", - "eslint": "ESLINT_USE_FLAT_CONFIG=false eslint --cache", + "eslint": "ESLINT_USE_FLAT_CONFIG=false eslint", "lint": "run-p 'lint:*'", "lint:es": "eslint . --cache", "lint:tsc": "tsc --noEmit", @@ -50,7 +50,7 @@ "test:dotInclude": "yarn eslint --ext ts,tsx tests/dotInclude --ignore-pattern \"!.dot\"", "test:dotPaths": "yarn eslint --ext ts,tsx tests/dotPaths --ignore-pattern \"!.dot\"", "test:dotProject": "yarn eslint --ext ts,tsx tests/dotProject --ignore-pattern \"!.dot\"", - "test:importXResolverV3": "eslint --cache --config=tests/importXResolverV3/eslint.config.cjs tests/importXResolverV3", + "test:importXResolverV3": "eslint --config=tests/importXResolverV3/eslint.config.cjs tests/importXResolverV3", "test:multipleEslintrcs": "yarn eslint --ext ts,tsx tests/multipleEslintrcs", "test:multipleTsconfigs": "yarn eslint --ext ts,tsx tests/multipleTsconfigs", "test:nearestTsconfig": "yarn eslint --ext ts,tsx tests/nearestTsconfig", From 2c797a9923becc438def1ccedc1742b8255eeb6e Mon Sep 17 00:00:00 2001 From: JounQin Date: Mon, 17 Mar 2025 07:24:34 +0800 Subject: [PATCH 7/7] fix: improve Windows compatibility --- .changeset/friendly-weeks-act.md | 2 +- src/helpers.ts | 5 +++++ src/index.ts | 5 +++-- src/normalize-options.ts | 14 +++++++++++--- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.changeset/friendly-weeks-act.md b/.changeset/friendly-weeks-act.md index 9fe7899..2ef9b97 100644 --- a/.changeset/friendly-weeks-act.md +++ b/.changeset/friendly-weeks-act.md @@ -2,7 +2,7 @@ "eslint-import-resolver-typescript": major --- -feat!: rewrite, speed up by using `rspack-resolver` which supports `references` natively under the hood +feat!: rewrite, speed up by using [`rspack-resolver`](https://github.com/unrs/rspack-resolver) which supports `references` natively under the hood BREAKING CHANGES: diff --git a/src/helpers.ts b/src/helpers.ts index 9e83cdd..4bc993e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -66,3 +66,8 @@ export const sortProjectsByAffinity = (projects: string[], file: string) => { .sort((a, b) => a.affinity - b.affinity) .map(item => item.project) } + +export const toGlobPath = (pathname: string) => pathname.replaceAll('\\', '/') + +export const toNativePath = (pathname: string) => + '/' === path.sep ? pathname : pathname.replaceAll('/', '\\') diff --git a/src/index.ts b/src/index.ts index 2ceb3f6..49cc9ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,9 +89,10 @@ export const resolve = ( if (!resolver) { const optionsHash = stableHash(options) - options = normalizeOptions(options) + const cwd = process.cwd() + options = normalizeOptions(options, cwd) // take `cwd` into account -- #217 - const cacheKey = `${optionsHash}:${process.cwd()}` + const cacheKey = `${optionsHash}:${cwd}` let cached = resolverCache.get(cacheKey) if (!cached && !options.project) { resolverCache.set(cacheKey, (cached = new ResolverFactory(options))) diff --git a/src/normalize-options.ts b/src/normalize-options.ts index 2a1326c..62a354b 100644 --- a/src/normalize-options.ts +++ b/src/normalize-options.ts @@ -10,7 +10,7 @@ import { defaultExtensions, defaultMainFields, } from './constants.js' -import { tryFile } from './helpers.js' +import { toGlobPath, toNativePath, tryFile } from './helpers.js' import { log } from './logger.js' import type { TypeScriptResolverOptions } from './types.js' @@ -22,10 +22,12 @@ let warned: boolean | undefined export function normalizeOptions( options?: TypeScriptResolverOptions | null, + cwd?: string, ): TypeScriptResolverOptions // eslint-disable-next-line sonarjs/cognitive-complexity export function normalizeOptions( options?: TypeScriptResolverOptions | null, + cwd = process.cwd(), ): TypeScriptResolverOptions { let { project, tsconfig, noWarnOnMultipleProjects } = (options ||= {}) @@ -37,16 +39,22 @@ export function normalizeOptions( configFile = tryFile(configFile) ensured = true } else if (project) { - project = Array.isArray(project) ? project : [project] + log('original projects:', ...project) + project = (Array.isArray(project) ? project : [project]).map(toGlobPath) if (project.some(p => isDynamicPattern(p))) { project = globSync(project, { absolute: true, + cwd, dot: true, + expandDirectories: false, onlyFiles: false, ignore: DEFAULT_IGNORE, }) } - project = project.flatMap(p => tryFile(DEFAULT_TRY_PATHS, false, p) || []) + log('resolving projects:', ...project) + project = project.flatMap( + p => tryFile(DEFAULT_TRY_PATHS, false, toNativePath(p)) || [], + ) log('resolved projects:', ...project) if (project.length === 1) { configFile = project[0]