UNPKG

eslint-import-resolver-next

Version:

The next resolver for `eslint-plugin-import` or `eslint-plugin-import-x`

691 lines (683 loc) 21.5 kB
import process$1 from 'node:process'; import { isBunBuiltin } from 'is-bun-module'; import path from 'node:path'; import { ResolverFactory } from 'unrs-resolver'; import fs from 'node:fs'; import module from 'node:module'; import yaml from 'js-yaml'; import { stableHash } from 'stable-hash'; import { globSync } from 'tinyglobby'; const defaultPackagesOptions = { patterns: ["."], ignore: [ "**/node_modules/**", "**/bower_components/**", "**/test/**", "**/tests/**", ], includeRoot: false, }; const defaultConfigFileOptions = { references: "auto", ignore: ["**/node_modules/**"], }; /** * Copy from https://github.com/9romise/eslint-import-resolver-oxc/blob/main/src/normalizeOptions.ts */ const defaultOptions = { aliasFields: [["browser"]], conditionNames: [ "types", "import", // APF: https://angular.io/guide/angular-package-format "esm2020", "es2020", "es2015", "require", "node", "node-addons", "browser", "default", ], extensionAlias: { ".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"], ".ts": [".ts", ".d.ts", ".js"], ".tsx": [ ".tsx", ".d.ts", ".jsx", // `.tsx` can also be compiled as `.js` ".js", ], ".cts": [".cts", ".d.cts", ".cjs"], ".mts": [".mts", ".d.mts", ".mjs"], }, extensions: [".ts", ".tsx", ".d.ts", ".js", ".jsx", ".json", ".node"], mainFields: [ "types", "typings", // APF: https://angular.io/guide/angular-package-format "fesm2020", "fesm2015", "esm2020", "es2020", "module", "jsnext:main", "main", "browser", ], tsconfig: true, jsconfig: true, bun: !!process.versions.bun, }; const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml"; const JSCONFIG_FILENAME = "jsconfig.json"; const TSCONFIG_FILENAME = "tsconfig.json"; /** * Whether to disable the cache or not. */ const isCacheDisabled = () => !!process$1.env.NEXT_RESOLVER_CACHE_DISABLED; const pathToPackagesCache = new Map(); function getPackagesCache(root) { if (isCacheDisabled()) return null; if (pathToPackagesCache.has(root)) { return pathToPackagesCache.get(root); } return null; } function setPackagesCache(root, packagePaths) { if (isCacheDisabled()) return; pathToPackagesCache.set(root, packagePaths); } const configFilesCache = new Map(); function getConfigFilesCache(root) { if (isCacheDisabled()) return null; if (configFilesCache.has(root)) { return configFilesCache.get(root); } return null; } function setConfigFilesCache(root, configFiles) { if (isCacheDisabled()) return; configFilesCache.set(root, configFiles); } const yamlCache = new Map(); function getYamlCache(root) { if (isCacheDisabled()) return null; if (yamlCache.has(root)) { return yamlCache.get(root); } return null; } function setYamlCache(root, yaml) { if (isCacheDisabled()) return; yamlCache.set(root, yaml); } /** * Remove duplicates from an array. * * @param {T[]} arr - the array to remove duplicates from * * @returns {T[]} the array without duplicates */ function unique(arr) { return Array.from(new Set(arr)); } /** * Check if the module has a Node.js prefix. * * @param modulePath - the module path * @returns {boolean} true if the module has a Node.js prefix, false otherwise */ function hasNodePrefix(modulePath) { return modulePath.startsWith("node:"); } /** * Check if the module has a Bun prefix. * * @param modulePath - the module path * @returns {boolean} true if the module has a Bun prefix, false otherwise */ function hasBunPrefix(modulePath) { return modulePath.startsWith("bun:"); } /** * Remove querystrings from the module path. * * Some imports may have querystrings, for example: * * import "foo?bar"; * * @param {string} modulePath - the import module path * * @returns {string} cleaned module path */ function removeQueryString(modulePath) { const querystringIndex = modulePath.indexOf("?"); if (querystringIndex > -1) { return modulePath.slice(0, querystringIndex); } return modulePath; } /** * Check if the module is a built-in module with a prefix. * * @param {string} modulePath - the module path * * @returns {boolean} true if the module is a built-in module with a prefix, false otherwise */ function isNodeBuiltin(modulePath) { let cleanedPath = modulePath; if (hasNodePrefix(modulePath)) { cleanedPath = modulePath.slice(5); } return module.builtinModules.includes(cleanedPath); } /** * Normalize patterns to include all possible package descriptor files. * * @param {string[]} patterns - the patterns to normalize * * @returns {string[]} the normalized patterns */ function normalizePatterns(patterns) { if (!patterns.length) return []; return patterns.flatMap((pattern) => { const convertedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, ""); // for some reason, fast-glob is buggy with /package.{json,yaml,json5} pattern return [ `${convertedPattern}/package.json`, `${convertedPattern}/package.json5`, `${convertedPattern}/package.yaml`, ]; }); } /** * Get the depth of a path. * * @param {string} p - the path * * @returns {number} the depth of the path */ function getPathDepth(p) { if (!p || p === path.sep) return 0; // if the path is windows absolute path, we need to remove the drive letter if (path.isAbsolute(p) && /^[a-zA-Z]:[/\\]/.test(p)) { p = p.slice(2); } return p.split(path.sep).filter(Boolean).length; } /** * Sort paths by the depth of the path. The deeper the path, the higher the priority. * * @param {string[]} paths - the paths to sort * * @returns {string[]} the sorted paths */ function sortPathsByDepth(paths) { return paths.sort((a, b) => { if (a === "/") return 1; if (b === "/") return -1; const aDepth = getPathDepth(a); const bDepth = getPathDepth(b); if (aDepth !== bDepth) { return bDepth - aDepth; } return b.localeCompare(a); }); } /** * Read a yaml file. * * @param {string} filePath - the file path to read * * @returns {T | null} the parsed yaml file */ function readYamlFile(filePath) { const cache = getYamlCache(filePath); if (cache) return cache; if (!fs.existsSync(filePath)) { return null; } let doc; try { doc = yaml.load(fs.readFileSync(filePath, "utf8")); doc ?? (doc = null); } catch { doc = null; } setYamlCache(filePath, doc); return doc; } /** * Normalize package glob options. * * @param {PackageOptions | string[]} opts - the package options * @param {string} root - the root path * * @returns {PackageGlobOptions} the normalized package glob options */ function normalizePackageGlobOptions(opts, root) { const { pnpmWorkspace, patterns, ...restOptions } = Array.isArray(opts) ? { patterns: opts } : opts; let mergedPatterns = []; if (pnpmWorkspace) { const pnpmWorkspacePath = path.join(root, typeof pnpmWorkspace === "string" ? pnpmWorkspace : PNPM_WORKSPACE_FILENAME); const pnpmWorkspaceRes = readYamlFile(pnpmWorkspacePath); if (pnpmWorkspaceRes?.packages?.length) { mergedPatterns.push(...pnpmWorkspaceRes.packages); } } if (patterns) { // the patterns in the options have higher priority mergedPatterns.push(...patterns); } mergedPatterns = unique(mergedPatterns.filter(Boolean)); if (mergedPatterns.length) { return { patterns: mergedPatterns, ...restOptions, }; } // return the original options if no patterns are found return { patterns, ...restOptions }; } /** * Find all packages in the root path. * * Copy from https://github.com/pnpm/pnpm/blob/b8b0c687f2e3403d07381822fe81c08478413916/fs/find-packages/src/index.ts * * @param {string} root - the root path * @param {PackageOptions | string[]} packageOpts - the package options * * @returns {string[]} the found package paths */ function findAllPackages(root, packageOpts) { const cache = getPackagesCache(root); if (cache) return cache; const opts = normalizePackageGlobOptions(packageOpts, root); const { patterns, includeRoot, ignore } = { ...defaultPackagesOptions, ...opts, }; const searchPatterns = patterns ?? defaultPackagesOptions.patterns; if (includeRoot) { searchPatterns.push("."); } const normalizedPatterns = normalizePatterns(searchPatterns); if (!normalizedPatterns.length) return []; const paths = globSync(normalizedPatterns, { cwd: root, ignore, expandDirectories: false, }); const packagePaths = unique(paths.map((manifestPath) => path.join(root, path.dirname(manifestPath)))); setPackagesCache(root, packagePaths); return packagePaths; } /** * Find the closest package from the source file. * * @param {string} sourceFile - the source file * @param {string[]} sortedPaths - the paths to search * * @returns {string | undefined} the closest package root */ function findClosestPackageRoot(sourceFile, sortedPaths) { return sortedPaths.find((p) => sourceFile.startsWith(p)); } /** * Sort config files by depth and specific filename. * * @param {string[]} configFiles - the config files to sort * @param {string} tsconfigFilename - the TypeScript config filename * * @returns {string[]} the sorted config files */ function sortConfigFiles(configFiles, tsconfigFilename) { return configFiles.sort((a, b) => { const aDepth = getPathDepth(a); const bDepth = getPathDepth(b); if (aDepth !== bDepth) { return bDepth - aDepth; } // move tsconfig before jsconfig if (tsconfigFilename) { if (a.endsWith(tsconfigFilename)) return -1; if (b.endsWith(tsconfigFilename)) return 1; } return b.localeCompare(a); }); } /** * Find the closest config file from the source file. * * @param {string} sourceFile - the source file * @param {string[]} sortedConfigFiles - the config files to search * * @returns {string | undefined} the closest config file */ function findClosestConfigFile(sourceFile, sortedConfigFiles) { return sortedConfigFiles.find((p) => sourceFile.startsWith(path.dirname(p))); } /** * Get the config files in the specified directory. * * @param {boolean | string | ConfigFileOptions | undefined} config - the config option * @param {string} root - the root path * @param {{ ignore?: string[]; filename: string }} defaults - the default options * * @returns {[string | undefined, string[] | undefined]} the filename and config files */ function getConfigFiles(config, root, defaults) { // if the config is not set, return undefined if (!config) return [undefined, undefined]; let filename; let ignore; if (typeof config === "object") { ignore = config.ignore ?? defaults.ignore; if (config.configFile) { if (path.isAbsolute(config.configFile)) { // if the config file is absolute, return the filename and the config file return [path.basename(config.configFile), [config.configFile]]; } else { filename = path.basename(config.configFile); } } else { filename = defaults.filename; } } else if (typeof config === "string") { filename = path.basename(config); ignore = defaults.ignore; } else { // if the config is set to true, use the default filename filename = defaults.filename; ignore = defaults.ignore; } const globPaths = globSync(`**/${filename}`, { cwd: root, ignore, expandDirectories: false, }); return [filename, globPaths.map((p) => path.join(root, p))]; } /** * Normalize the config file options. * * @param {Record<"tsconfig" | "jsconfig", boolean | string | ConfigFileOptions | undefined>} configs - the config file options * @param {string} packageDir - the directory of the package * @param {string} sourceFile - the source file * * @returns {ConfigFileOptions | undefined} the normalized config file options */ function normalizeConfigFileOptions(configs, packageDir, sourceFile) { const { jsconfig, tsconfig } = configs; if (!tsconfig && !jsconfig) return undefined; const { ignore: defaultIgnore, ...restDefaultOptions } = defaultConfigFileOptions; let configFiles = getConfigFilesCache(packageDir); if (!configFiles) { const globFiles = []; const [tsconfigFilename, tsconfigFiles] = getConfigFiles(tsconfig, packageDir, { ignore: defaultIgnore, filename: TSCONFIG_FILENAME }); if (tsconfigFiles) { globFiles.push(...tsconfigFiles); } const [, jsconfigFiles] = getConfigFiles(jsconfig, packageDir, { ignore: defaultIgnore, filename: JSCONFIG_FILENAME, }); if (jsconfigFiles) { globFiles.push(...jsconfigFiles); } configFiles = sortConfigFiles(globFiles, tsconfigFilename); setConfigFilesCache(packageDir, configFiles); } if (!configFiles.length) { return undefined; } if (configFiles.length === 1) { return { ...restDefaultOptions, configFile: configFiles[0] }; } const closestConfigPath = findClosestConfigFile(sourceFile, configFiles); if (closestConfigPath) { return { ...restDefaultOptions, configFile: closestConfigPath }; } return undefined; } /** * Normalize the alias mapping. * * @param {Record<string, string | string[]> | undefined} alias - the alias mapping * @param {string} parent - the parent directory * * @returns {Record<string, string[]> | undefined} the normalized alias mapping */ function normalizeAlias(alias, parent) { if (!alias) return undefined; const normalizedAlias = Object.keys(alias).reduce((acc, key) => { const value = Array.isArray(alias[key]) ? alias[key] : [alias[key]]; acc[key] = value.map((item) => { if (path.isAbsolute(item)) { return item; } return path.resolve(parent, item); }); return acc; }, {}); return normalizedAlias; } /** * Get the hash of an object. * * @param {unknown} obj - the object to hash * * @returns {string} the hash of the object */ function hashObject(obj) { return stableHash(obj); } /** * Find all workspace packages. * * @param {string[]} roots - the roots to search * @param {string[] | PackageOptions} packages - the package options * * @returns {string[]} the sorted workspace packages */ function findWorkspacePackages(roots, packages) { if (packages && typeof packages === "object") { const find = roots.flatMap((r) => findAllPackages(r, packages)); return sortPathsByDepth(unique(find)); } return sortPathsByDepth([...roots]); } let relativeResolver = null; function getRelativeResolver(options) { relativeResolver ?? (relativeResolver = new ResolverFactory(options)); return relativeResolver; } const MAX_RESOLVER_CACHE_SIZE = 4; const resolverMap = new Map(); function getResolver(hashKey, options) { if (resolverMap.has(hashKey)) { return resolverMap.get(hashKey); } if (resolverMap.size >= MAX_RESOLVER_CACHE_SIZE) { const firstKey = resolverMap.keys().next().value; const oldResolver = resolverMap.get(firstKey); oldResolver.clearCache(); resolverMap.delete(firstKey); } const resolver = new ResolverFactory(options); resolverMap.set(hashKey, resolver); return resolver; } /** * Resolves relative path imports * * @param sourceFile - The file that imports the module * @param modulePath - The module path to resolve * @param options - The resolver options * @returns */ function resolveRelativePath(sourceFile, modulePath, options) { const sourceFileDir = path.dirname(sourceFile); const relativeResolver = getRelativeResolver(options); const result = relativeResolver.sync(sourceFileDir, modulePath); if (result.path) { return { found: true, path: result.path }; } return { found: false }; } /** * Resolves a module path * * @param modulePath - The module path to resolve * @param sourceFile - The file that imports the module * @param options - The resolver options * @returns */ function resolveModulePath(sourceFile, modulePath, options) { // hash the options to cache the resolver // other options are not needed as they are not usually changed const hashKey = hashObject({ alias: options.alias, tsconfig: options.tsconfig, roots: options.roots, }); const resolver = getResolver(hashKey, options); const result = resolver.sync(path.dirname(sourceFile), modulePath); if (result.path) { return { found: true, path: result.path }; } return { found: false }; } function checkBuiltinModule(modulePath, isBun = false) { if (isBun) { if (isBunBuiltin(modulePath)) { return { found: true, path: null }; } if (hasNodePrefix(modulePath) || hasBunPrefix(modulePath)) { return { found: false }; } } else { if (hasNodePrefix(modulePath)) { const result = isNodeBuiltin(modulePath); return result ? { found: true, path: null } : { found: false }; } if (hasBunPrefix(modulePath)) { return { found: false }; } if (isNodeBuiltin(modulePath)) { return { found: true, path: null }; } } } function resolve(modulePath, sourceFile, config) { const cleanedPath = removeQueryString(modulePath); const { roots, alias, packages, jsconfig, tsconfig, bun, ...restOptions } = { ...defaultOptions, ...config, }; const result = checkBuiltinModule(cleanedPath, bun); if (result) { return result; } if (hasNodePrefix(cleanedPath) || hasBunPrefix(cleanedPath)) { const result = isNodeBuiltin(cleanedPath); return result ? { found: true, path: null } : { found: false }; } // relative path if (modulePath.startsWith(".")) { return resolveRelativePath(sourceFile, modulePath, restOptions); } const resolveRoots = roots?.length ? roots : [process$1.cwd()]; const workspacePackages = findWorkspacePackages(resolveRoots, packages); const packageDir = findClosestPackageRoot(sourceFile, workspacePackages); // file not find in any package if (!packageDir) { return { found: false }; } const packageRoots = unique([packageDir, ...resolveRoots]); const resolveAlias = normalizeAlias(alias, packageDir); const configFileOptions = normalizeConfigFileOptions({ tsconfig, jsconfig }, packageDir, sourceFile); return resolveModulePath(sourceFile, modulePath, { alias: resolveAlias, tsconfig: configFileOptions, roots: packageRoots, ...restOptions, }); } function createNextImportResolver(config) { const { roots, alias, packages, jsconfig, tsconfig, bun, ...restOptions } = { ...defaultOptions, ...config, }; const resolveRoots = roots?.length ? roots : [process$1.cwd()]; const workspacePackages = findWorkspacePackages(resolveRoots, packages); return { interfaceVersion: 3, name: "eslint-import-resolver-next", resolve: (modulePath, sourceFile) => { const cleanedPath = removeQueryString(modulePath); const result = checkBuiltinModule(cleanedPath, bun); if (result) { return result; } const packageDir = findClosestPackageRoot(sourceFile, workspacePackages); // file not find in any package if (!packageDir) { return { found: false }; } const packageRoots = unique([packageDir, ...resolveRoots]); const resolveAlias = normalizeAlias(alias, packageDir); const configFileOptions = normalizeConfigFileOptions({ tsconfig, jsconfig }, packageDir, sourceFile); return resolveModulePath(sourceFile, modulePath, { alias: resolveAlias, tsconfig: configFileOptions, roots: packageRoots, ...restOptions, }); }, }; } const interfaceVersion = 2; var index = { interfaceVersion, resolve, }; export { createNextImportResolver, index as default, interfaceVersion, resolve };