eslint-import-resolver-next
Version:
The next resolver for `eslint-plugin-import` or `eslint-plugin-import-x`
691 lines (683 loc) • 21.5 kB
JavaScript
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 };