UNPKG

eslint-plugin-perfectionist

Version:

ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.

201 lines (200 loc) 5.98 kB
import { matches } from '../../utils/matches.js' import { getTypescriptImport } from './get-typescript-import.js' import { matchesTsconfigPaths } from './matches-tsconfig-paths.js' import { builtinModules } from 'node:module' /** * Computes all applicable selectors for an import based on its module path. * * Analyzes an import's module specifier to determine which categories it * belongs to, such as external package, internal module, relative import, or * Node.js builtin. These selectors are used for grouping and sorting imports * according to user configuration. * * @param options - Configuration options. * @param options.tsConfigOutput - TypeScript configuration for path resolution. * @param options.filename - Current file path for relative import resolution. * @param options.options - Rule options including internal patterns and * environment. * @param options.name - Import module specifier to analyze. * @returns Array of matching selectors for the import. */ function computeCommonSelectors({ tsConfigOutput, filename, options, name }) { function matchesInternalPattern(value) { return options.internalPattern.some(pattern => matches(value, pattern)) } let internalExternalGroup = matchesInternalPattern(name) ? 'internal' : ( getInternalOrExternalGroup({ tsConfigOutput, filename, name, }) ) let commonSelectors = [] if ( tsConfigOutput && matchesTsconfigPaths({ tsConfigOutput, name, }) ) { commonSelectors.push('tsconfig-path') } if (isIndex(name)) { commonSelectors.push('index') } if (isSibling(name)) { commonSelectors.push('sibling') } if (isParent(name)) { commonSelectors.push('parent') } if (isSubpath(name)) { commonSelectors.push('subpath') } if (internalExternalGroup === 'internal') { commonSelectors.push('internal') } if (isCoreModule(name, options.environment)) { commonSelectors.push('builtin') } if (internalExternalGroup === 'external') { commonSelectors.push('external') } return commonSelectors } var bunModules = /* @__PURE__ */ new Set([ 'detect-libc', 'bun:sqlite', 'bun:test', 'bun:wrap', 'bun:ffi', 'bun:jsc', 'undici', 'bun', 'ws', ]) var nodeBuiltinModules = new Set(builtinModules) var builtinPrefixOnlyModules = /* @__PURE__ */ new Set([ 'node:sqlite', 'node:test', 'node:sea', ]) /** * Determines whether an import is internal or external to the project. * * Uses TypeScript's module resolution to accurately classify imports based on * whether they resolve to external libraries (node_modules) or internal project * files. Falls back to path-based heuristics when TypeScript is unavailable. * * @param options - Options for determining the group. * @param options.tsConfigOutput - TypeScript configuration for module * resolution. * @param options.filename - Current file path for resolution context. * @param options.name - Import module specifier to classify. * @returns 'internal' for project modules, 'external' for dependencies, null * for relative imports. */ function getInternalOrExternalGroup({ tsConfigOutput, filename, name }) { let typescriptImport = getTypescriptImport() if (!typescriptImport) { return !name.startsWith('.') && !name.startsWith('/') ? 'external' : null } if (typescriptImport.isExternalModuleNameRelative(name)) { return null } if (!tsConfigOutput) { return 'external' } let resolution = typescriptImport.resolveModuleName( name, filename, tsConfigOutput.compilerOptions, typescriptImport.sys, tsConfigOutput.cache, ) if (typeof resolution.resolvedModule?.isExternalLibraryImport !== 'boolean') { return 'external' } return resolution.resolvedModule.isExternalLibraryImport ? 'external' : 'internal' } /** * Checks if a module is a Node.js or Bun builtin module. * * Handles various formats including 'node:' prefixed modules and subpath * imports (e.g., 'fs/promises'). Also supports Bun-specific modules when in Bun * environment. * * @param value - Module specifier to check. * @param environment - Runtime environment ('node' or 'bun'). * @returns True if the module is a builtin module. */ function isCoreModule(value, environment) { function clean(string_) { return string_.replace(/^(?:node:){1,2}/u, '') } let [basePath] = value.split('/') let cleanValue = clean(value) let cleanBase = clean(basePath) if (nodeBuiltinModules.has(cleanValue) || nodeBuiltinModules.has(cleanBase)) { return true } if ( builtinPrefixOnlyModules.has(value) || builtinPrefixOnlyModules.has(`node:${cleanValue}`) || builtinPrefixOnlyModules.has(basePath) || builtinPrefixOnlyModules.has(`node:${cleanBase}`) ) { return true } return environment === 'bun' && bunModules.has(value) } /** * Checks if an import is an index file import. * * @param value - Import path to check. * @returns True if importing an index file. */ function isIndex(value) { return [ './index.d.js', './index.d.ts', './index.js', './index.ts', './index', './', '.', ].includes(value) } /** * Checks if an import is a sibling module (same directory). * * @param value - Import path to check. * @returns True if importing from the same directory. */ function isSibling(value) { return value.startsWith('./') } /** * Checks if an import is from a parent directory. * * @param value - Import path to check. * @returns True if importing from a parent directory. */ function isParent(value) { return value.startsWith('..') } /** * Checks if an import is a Node.js subpath import. * * Subpath imports start with '#' and are defined in package.json imports field. * * @param value - Import path to check. * @returns True if using subpath import syntax. */ function isSubpath(value) { return value.startsWith('#') } export { computeCommonSelectors }