diff --git a/README.md b/README.md index 238c5347a..81bfbc795 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,7 @@ settings: [`eslint_d`]: https://www.npmjs.com/package/eslint_d [`eslint-loader`]: https://www.npmjs.com/package/eslint-loader + ## SublimeLinter-eslint SublimeLinter-eslint introduced a change to support `.eslintignore` files diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 069dab0ce..5c3542ebd 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -35,6 +35,13 @@ Also there is one more option called `packageDir`, this option is to specify the "import/no-extraneous-dependencies": ["error", {"packageDir": './some-dir/'}] ``` +It may also be an array of multiple paths, to support monorepos or other novel project +folder layouts: + +```js +"import/no-extraneous-dependencies": ["error", {"packageDir": ['./some-dir/', './root-pkg']}] +``` + ## Rule Details Given the following `package.json`: diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index bb684e448..9d51018e9 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -1,5 +1,6 @@ import path from 'path' import fs from 'fs' +import { isArray, isEmpty } from 'lodash' import readPkgUp from 'read-pkg-up' import minimatch from 'minimatch' import resolve from 'eslint-module-utils/resolve' @@ -7,24 +8,66 @@ import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' import docsUrl from '../docsUrl' +function hasKeys(obj = {}) { + return Object.keys(obj).length > 0 +} + +function extractDepFields(pkg) { + return { + dependencies: pkg.dependencies || {}, + devDependencies: pkg.devDependencies || {}, + optionalDependencies: pkg.optionalDependencies || {}, + peerDependencies: pkg.peerDependencies || {}, + } +} + function getDependencies(context, packageDir) { + let paths = [] try { - const packageContent = packageDir - ? JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) - : readPkgUp.sync({cwd: context.getFilename(), normalize: false}).pkg + const packageContent = { + dependencies: {}, + devDependencies: {}, + optionalDependencies: {}, + peerDependencies: {}, + } - if (!packageContent) { - return null + if (!isEmpty(packageDir)) { + if (!isArray(packageDir)) { + paths = [path.resolve(packageDir)] + } else { + paths = packageDir.map(dir => path.resolve(dir)) + } } - return { - dependencies: packageContent.dependencies || {}, - devDependencies: packageContent.devDependencies || {}, - optionalDependencies: packageContent.optionalDependencies || {}, - peerDependencies: packageContent.peerDependencies || {}, + if (!isEmpty(paths)) { + // use rule config to find package.json + paths.forEach(dir => { + Object.assign(packageContent, extractDepFields( + JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) + )) + }) + } else { + // use closest package.json + Object.assign( + packageContent, + extractDepFields( + readPkgUp.sync({cwd: context.getFilename(), normalize: false}).pkg + ) + ) + } + + if (![ + packageContent.dependencies, + packageContent.devDependencies, + packageContent.optionalDependencies, + packageContent.peerDependencies, + ].some(hasKeys)) { + return null } + + return packageContent } catch (e) { - if (packageDir && e.code === 'ENOENT') { + if (!isEmpty(paths) && e.code === 'ENOENT') { context.report({ message: 'The package.json file could not be found.', loc: { line: 0, column: 0 }, @@ -66,9 +109,8 @@ function reportIfMissing(context, deps, depsOptions, node, name) { } const resolved = resolve(name, context) - if (!resolved) { - return - } + if (!resolved) { return } + const splitName = name.split('/') const packageName = splitName[0][0] === '@' ? splitName.slice(0, 2).join('/') @@ -124,7 +166,7 @@ module.exports = { 'devDependencies': { 'type': ['boolean', 'array'] }, 'optionalDependencies': { 'type': ['boolean', 'array'] }, 'peerDependencies': { 'type': ['boolean', 'array'] }, - 'packageDir': { 'type': 'string' }, + 'packageDir': { 'type': ['string', 'array'] }, }, 'additionalProperties': false, }, diff --git a/tests/files/monorepo/package.json b/tests/files/monorepo/package.json new file mode 100644 index 000000000..3ed889ddf --- /dev/null +++ b/tests/files/monorepo/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "devDependencies": { + "left-pad": "^1.2.0" + } +} diff --git a/tests/files/monorepo/packages/nested-package/package.json b/tests/files/monorepo/packages/nested-package/package.json new file mode 100644 index 000000000..615c0a234 --- /dev/null +++ b/tests/files/monorepo/packages/nested-package/package.json @@ -0,0 +1,6 @@ +{ + "name": "nested-monorepo-pkg", + "dependencies": { + "react": "^16.0.0" + } +} diff --git a/tests/files/node_modules/left-pad b/tests/files/node_modules/left-pad new file mode 120000 index 000000000..dbbbe75d2 --- /dev/null +++ b/tests/files/node_modules/left-pad @@ -0,0 +1 @@ +not-a-dependency \ No newline at end of file diff --git a/tests/files/node_modules/react b/tests/files/node_modules/react new file mode 120000 index 000000000..dbbbe75d2 --- /dev/null +++ b/tests/files/node_modules/react @@ -0,0 +1 @@ +not-a-dependency \ No newline at end of file diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index a8817931b..381b392cb 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -15,6 +15,8 @@ const packageFileWithSyntaxErrorMessage = (() => { } })() const packageDirWithFlowTyped = path.join(__dirname, '../../files/with-flow-typed') +const packageDirMonoRepoRoot = path.join(__dirname, '../../files/monorepo') +const packageDirMonoRepoWithNested = path.join(__dirname, '../../files/monorepo/packages/nested-package') ruleTester.run('no-extraneous-dependencies', rule, { valid: [ @@ -75,8 +77,46 @@ ruleTester.run('no-extraneous-dependencies', rule, { options: [{packageDir: packageDirWithFlowTyped}], parser: 'babel-eslint', }), + test({ + code: 'import react from "react";', + options: [{packageDir: packageDirMonoRepoWithNested}], + }), + test({ + code: 'import leftpad from "left-pad";', + options: [{packageDir: [packageDirMonoRepoWithNested, packageDirMonoRepoRoot]}], + }), + test({ + code: 'import leftpad from "left-pad";', + options: [{packageDir: packageDirMonoRepoRoot}], + }), ], invalid: [ + test({ + code: 'import "not-a-dependency"', + filename: path.join(packageDirMonoRepoRoot, 'foo.js'), + options: [{packageDir: packageDirMonoRepoRoot }], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: '\'not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it', + }], + }), + test({ + code: 'import "not-a-dependency"', + filename: path.join(packageDirMonoRepoWithNested, 'foo.js'), + options: [{packageDir: packageDirMonoRepoRoot}], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: '\'not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it', + }], + }), + test({ + code: 'import "not-a-dependency"', + options: [{packageDir: packageDirMonoRepoRoot}], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: '\'not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it', + }], + }), test({ code: 'import "not-a-dependency"', errors: [{ @@ -197,5 +237,31 @@ ruleTester.run('no-extraneous-dependencies', rule, { message: 'The package.json file could not be parsed: ' + packageFileWithSyntaxErrorMessage, }], }), - ], + test({ + code: 'import leftpad from "left-pad";', + filename: path.join(packageDirMonoRepoWithNested, 'foo.js'), + options: [{packageDir: packageDirMonoRepoWithNested}], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: "'left-pad' should be listed in the project's dependencies. Run 'npm i -S left-pad' to add it", + }], + }), + test({ + code: 'import react from "react";', + filename: path.join(packageDirMonoRepoRoot, 'foo.js'), + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: "'react' should be listed in the project's dependencies. Run 'npm i -S react' to add it", + }], + }), + test({ + code: 'import react from "react";', + filename: path.join(packageDirMonoRepoWithNested, 'foo.js'), + options: [{packageDir: packageDirMonoRepoRoot}], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: "'react' should be listed in the project's dependencies. Run 'npm i -S react' to add it", + }], + }), + ] })