UNPKG

eslint-plugin-import-x

Version:
275 lines 10.7 kB
import fs from 'node:fs'; import path from 'node:path'; import { minimatch } from 'minimatch'; import { createRule, moduleVisitor, resolve, pkgUp, importType, getFilePackageName, } from '../utils/index.js'; const depFieldCache = new Map(); function hasKeys(obj = {}) { return Object.keys(obj).length > 0; } function arrayOrKeys(arrayOrObject) { return Array.isArray(arrayOrObject) ? arrayOrObject : Object.keys(arrayOrObject); } function readJSON(jsonPath, throwException) { try { return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); } catch (error) { if (throwException) { throw error; } } } function extractDepFields(pkg) { return { dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, optionalDependencies: pkg.optionalDependencies || {}, peerDependencies: pkg.peerDependencies || {}, bundledDependencies: arrayOrKeys(pkg.bundleDependencies || pkg.bundledDependencies || []), }; } function getPackageDepFields(packageJsonPath, throwAtRead) { if (!depFieldCache.has(packageJsonPath)) { const packageJson = readJSON(packageJsonPath, throwAtRead); if (packageJson) { const depFields = extractDepFields(packageJson); depFieldCache.set(packageJsonPath, depFields); } } return depFieldCache.get(packageJsonPath); } function getDependencies(context, packageDir) { let paths = []; try { let packageContent = { dependencies: {}, devDependencies: {}, optionalDependencies: {}, peerDependencies: {}, bundledDependencies: [], }; if (packageDir && packageDir.length > 0) { paths = Array.isArray(packageDir) ? packageDir.map(dir => path.resolve(dir)) : [path.resolve(packageDir)]; } if (paths.length > 0) { for (const dir of paths) { const packageJsonPath = path.resolve(dir, 'package.json'); const packageContent_ = getPackageDepFields(packageJsonPath, paths.length === 1); if (packageContent_) { for (const depsKey of Object.keys(packageContent)) { const key = depsKey; Object.assign(packageContent[key], packageContent_[key]); } } } } else { const packageJsonPath = pkgUp({ cwd: context.physicalFilename, }); const packageContent_ = getPackageDepFields(packageJsonPath, false); if (packageContent_) { packageContent = packageContent_; } } if (![ packageContent.dependencies, packageContent.devDependencies, packageContent.optionalDependencies, packageContent.peerDependencies, packageContent.bundledDependencies, ].some(hasKeys)) { return; } return packageContent; } catch (error_) { const error = error_; if (paths.length > 0 && error.code === 'ENOENT') { context.report({ messageId: 'pkgNotFound', loc: { line: 0, column: 0 }, }); } if (error.name === 'JSONError' || error instanceof SyntaxError) { context.report({ messageId: 'pkgUnparsable', data: { error: error.message }, loc: { line: 0, column: 0 }, }); } } } function getModuleOriginalName(name) { const [first, second] = name.split('/'); return first.startsWith('@') ? `${first}/${second}` : first; } function checkDependencyDeclaration(deps, packageName, declarationStatus) { const newDeclarationStatus = declarationStatus || { isInDeps: false, isInDevDeps: false, isInOptDeps: false, isInPeerDeps: false, isInBundledDeps: false, }; const packageHierarchy = []; const packageNameParts = packageName ? packageName.split('/') : []; for (const [index, namePart] of packageNameParts.entries()) { if (!namePart.startsWith('@')) { const ancestor = packageNameParts.slice(0, index + 1).join('/'); packageHierarchy.push(ancestor); } } return packageHierarchy.reduce((result, ancestorName) => ({ isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined, isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined, isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined, isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined, isInBundledDeps: result.isInBundledDeps || deps.bundledDependencies.includes(ancestorName), }), newDeclarationStatus); } function reportIfMissing(context, deps, depsOptions, node, name, whitelist) { if (!depsOptions.verifyTypeImports && (('importKind' in node && (node.importKind === 'type' || node.importKind === 'typeof')) || ('exportKind' in node && node.exportKind === 'type') || ('specifiers' in node && Array.isArray(node.specifiers) && node.specifiers.length > 0 && node.specifiers.every(specifier => 'importKind' in specifier && (specifier.importKind === 'type' || specifier.importKind === 'typeof'))))) { return; } const typeOfImport = importType(name, context); if (typeOfImport !== 'external' && (typeOfImport !== 'internal' || !depsOptions.verifyInternalDeps)) { return; } const resolved = resolve(name, context); if (!resolved) { return; } const importPackageName = getModuleOriginalName(name); let declarationStatus = checkDependencyDeclaration(deps, importPackageName); if (declarationStatus.isInDeps || (depsOptions.allowDevDeps && declarationStatus.isInDevDeps) || (depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) || (depsOptions.allowOptDeps && declarationStatus.isInOptDeps) || (depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)) { return; } const realPackageName = getFilePackageName(resolved); if (realPackageName && realPackageName !== importPackageName) { declarationStatus = checkDependencyDeclaration(deps, realPackageName, declarationStatus); if (declarationStatus.isInDeps || (depsOptions.allowDevDeps && declarationStatus.isInDevDeps) || (depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) || (depsOptions.allowOptDeps && declarationStatus.isInOptDeps) || (depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)) { return; } } const packageName = realPackageName || importPackageName; if (whitelist?.has(packageName)) { return; } if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) { context.report({ node, messageId: 'devDep', data: { packageName, }, }); return; } if (declarationStatus.isInOptDeps && !depsOptions.allowOptDeps) { context.report({ node, messageId: 'optDep', data: { packageName, }, }); return; } context.report({ node, messageId: 'missing', data: { packageName, }, }); } function testConfig(config, filename) { if (typeof config === 'boolean' || config === undefined) { return config; } return config.some(c => minimatch(filename, c) || minimatch(filename, path.resolve(c), { windowsPathsNoEscape: true })); } export default createRule({ name: 'no-extraneous-dependencies', meta: { type: 'problem', docs: { category: 'Helpful warnings', description: 'Forbid the use of extraneous packages.', }, schema: [ { type: 'object', properties: { devDependencies: { type: ['boolean', 'array'] }, optionalDependencies: { type: ['boolean', 'array'] }, peerDependencies: { type: ['boolean', 'array'] }, bundledDependencies: { type: ['boolean', 'array'] }, packageDir: { type: ['string', 'array'] }, includeInternal: { type: ['boolean'] }, includeTypes: { type: ['boolean'] }, whitelist: { type: ['array'] }, }, additionalProperties: false, }, ], messages: { pkgNotFound: 'The package.json file could not be found.', pkgUnparsable: 'The package.json file could not be parsed: {{error}}', devDep: "'{{packageName}}' should be listed in the project's dependencies, not devDependencies.", optDep: "'{{packageName}}' should be listed in the project's dependencies, not optionalDependencies.", missing: "'{{packageName}}' should be listed in the project's dependencies. Run 'npm i -S {{packageName}}' to add it", }, }, defaultOptions: [], create(context) { const options = context.options[0] || {}; const filename = context.physicalFilename; const deps = getDependencies(context, options.packageDir) || extractDepFields({}); const depsOptions = { allowDevDeps: testConfig(options.devDependencies, filename) !== false, allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, allowBundledDeps: testConfig(options.bundledDependencies, filename) !== false, verifyInternalDeps: !!options.includeInternal, verifyTypeImports: !!options.includeTypes, }; return { ...moduleVisitor((source, node) => { reportIfMissing(context, deps, depsOptions, node, source.value, options.whitelist ? new Set(options.whitelist) : undefined); }, { commonjs: true }), 'Program:exit'() { depFieldCache.clear(); }, }; }, }); //# sourceMappingURL=no-extraneous-dependencies.js.map