Path: blob/main/src/rules/no-extraneous-dependencies.js
829 views
import path from 'path';1import fs from 'fs';2import readPkgUp from 'eslint-module-utils/readPkgUp';3import minimatch from 'minimatch';4import resolve from 'eslint-module-utils/resolve';5import moduleVisitor from 'eslint-module-utils/moduleVisitor';6import importType from '../core/importType';7import { getFilePackageName } from '../core/packagePath';8import docsUrl from '../docsUrl';910const depFieldCache = new Map();1112function hasKeys(obj = {}) {13return Object.keys(obj).length > 0;14}1516function arrayOrKeys(arrayOrObject) {17return Array.isArray(arrayOrObject) ? arrayOrObject : Object.keys(arrayOrObject);18}1920function extractDepFields(pkg) {21return {22dependencies: pkg.dependencies || {},23devDependencies: pkg.devDependencies || {},24optionalDependencies: pkg.optionalDependencies || {},25peerDependencies: pkg.peerDependencies || {},26// BundledDeps should be in the form of an array, but object notation is also supported by27// `npm`, so we convert it to an array if it is an object28bundledDependencies: arrayOrKeys(pkg.bundleDependencies || pkg.bundledDependencies || []),29};30}3132function getDependencies(context, packageDir) {33let paths = [];34try {35const packageContent = {36dependencies: {},37devDependencies: {},38optionalDependencies: {},39peerDependencies: {},40bundledDependencies: [],41};4243if (packageDir && packageDir.length > 0) {44if (!Array.isArray(packageDir)) {45paths = [path.resolve(packageDir)];46} else {47paths = packageDir.map(dir => path.resolve(dir));48}49}5051if (paths.length > 0) {52// use rule config to find package.json53paths.forEach(dir => {54const packageJsonPath = path.join(dir, 'package.json');55if (!depFieldCache.has(packageJsonPath)) {56const depFields = extractDepFields(57JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')),58);59depFieldCache.set(packageJsonPath, depFields);60}61const _packageContent = depFieldCache.get(packageJsonPath);62Object.keys(packageContent).forEach(depsKey =>63Object.assign(packageContent[depsKey], _packageContent[depsKey]),64);65});66} else {67// use closest package.json68Object.assign(69packageContent,70extractDepFields(71readPkgUp({ cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), normalize: false }).pkg,72),73);74}7576if (![77packageContent.dependencies,78packageContent.devDependencies,79packageContent.optionalDependencies,80packageContent.peerDependencies,81packageContent.bundledDependencies,82].some(hasKeys)) {83return null;84}8586return packageContent;87} catch (e) {88if (paths.length > 0 && e.code === 'ENOENT') {89context.report({90message: 'The package.json file could not be found.',91loc: { line: 0, column: 0 },92});93}94if (e.name === 'JSONError' || e instanceof SyntaxError) {95context.report({96message: 'The package.json file could not be parsed: ' + e.message,97loc: { line: 0, column: 0 },98});99}100101return null;102}103}104105function missingErrorMessage(packageName) {106return `'${packageName}' should be listed in the project's dependencies. ` +107`Run 'npm i -S ${packageName}' to add it`;108}109110function devDepErrorMessage(packageName) {111return `'${packageName}' should be listed in the project's dependencies, not devDependencies.`;112}113114function optDepErrorMessage(packageName) {115return `'${packageName}' should be listed in the project's dependencies, ` +116`not optionalDependencies.`;117}118119function getModuleOriginalName(name) {120const [first, second] = name.split('/');121return first.startsWith('@') ? `${first}/${second}` : first;122}123124function getModuleRealName(resolved) {125return getFilePackageName(resolved);126}127128function checkDependencyDeclaration(deps, packageName, declarationStatus) {129const newDeclarationStatus = declarationStatus || {130isInDeps: false,131isInDevDeps: false,132isInOptDeps: false,133isInPeerDeps: false,134isInBundledDeps: false,135};136137// in case of sub package.json inside a module138// check the dependencies on all hierarchy139const packageHierarchy = [];140const packageNameParts = packageName ? packageName.split('/') : [];141packageNameParts.forEach((namePart, index) => {142if (!namePart.startsWith('@')) {143const ancestor = packageNameParts.slice(0, index + 1).join('/');144packageHierarchy.push(ancestor);145}146});147148return packageHierarchy.reduce((result, ancestorName) => {149return {150isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined,151isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined,152isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined,153isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined,154isInBundledDeps:155result.isInBundledDeps || deps.bundledDependencies.indexOf(ancestorName) !== -1,156};157}, newDeclarationStatus);158}159160function reportIfMissing(context, deps, depsOptions, node, name) {161// Do not report when importing types162if (163node.importKind === 'type' ||164node.importKind === 'typeof'165) {166return;167}168169if (importType(name, context) !== 'external') {170return;171}172173const resolved = resolve(name, context);174if (!resolved) { return; }175176const importPackageName = getModuleOriginalName(name);177let declarationStatus = checkDependencyDeclaration(deps, importPackageName);178179if (180declarationStatus.isInDeps ||181(depsOptions.allowDevDeps && declarationStatus.isInDevDeps) ||182(depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) ||183(depsOptions.allowOptDeps && declarationStatus.isInOptDeps) ||184(depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)185) {186return;187}188189// test the real name from the resolved package.json190// if not aliased imports (alias/react for example), importPackageName can be misinterpreted191const realPackageName = getModuleRealName(resolved);192if (realPackageName && realPackageName !== importPackageName) {193declarationStatus = checkDependencyDeclaration(deps, realPackageName, declarationStatus);194195if (196declarationStatus.isInDeps ||197(depsOptions.allowDevDeps && declarationStatus.isInDevDeps) ||198(depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) ||199(depsOptions.allowOptDeps && declarationStatus.isInOptDeps) ||200(depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)201) {202return;203}204}205206if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) {207context.report(node, devDepErrorMessage(realPackageName || importPackageName));208return;209}210211if (declarationStatus.isInOptDeps && !depsOptions.allowOptDeps) {212context.report(node, optDepErrorMessage(realPackageName || importPackageName));213return;214}215216context.report(node, missingErrorMessage(realPackageName || importPackageName));217}218219function testConfig(config, filename) {220// Simplest configuration first, either a boolean or nothing.221if (typeof config === 'boolean' || typeof config === 'undefined') {222return config;223}224// Array of globs.225return config.some(c => (226minimatch(filename, c) ||227minimatch(filename, path.join(process.cwd(), c))228));229}230231module.exports = {232meta: {233type: 'problem',234docs: {235url: docsUrl('no-extraneous-dependencies'),236},237238schema: [239{240'type': 'object',241'properties': {242'devDependencies': { 'type': ['boolean', 'array'] },243'optionalDependencies': { 'type': ['boolean', 'array'] },244'peerDependencies': { 'type': ['boolean', 'array'] },245'bundledDependencies': { 'type': ['boolean', 'array'] },246'packageDir': { 'type': ['string', 'array'] },247},248'additionalProperties': false,249},250],251},252253create(context) {254const options = context.options[0] || {};255const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();256const deps = getDependencies(context, options.packageDir) || extractDepFields({});257258const depsOptions = {259allowDevDeps: testConfig(options.devDependencies, filename) !== false,260allowOptDeps: testConfig(options.optionalDependencies, filename) !== false,261allowPeerDeps: testConfig(options.peerDependencies, filename) !== false,262allowBundledDeps: testConfig(options.bundledDependencies, filename) !== false,263};264265return moduleVisitor((source, node) => {266reportIfMissing(context, deps, depsOptions, node, source.value);267}, { commonjs: true });268},269};270271272