Skip to content

Commit 62bfc20

Browse files
luxaritasljharb
authored andcommitted
[New] no-extraneous-dependencies: Add considerInParents option
1 parent 9799567 commit 62bfc20

File tree

5 files changed

+150
-21
lines changed

5 files changed

+150
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
5858
- [`prefer-default-export`]: add "target" option ([#2602], thanks [@azyzz228])
5959
- [`no-absolute-path`]: add fixer ([#2613], thanks [@adipascu])
6060
- [`no-duplicates`]: support inline type import with `inlineTypeImport` option ([#2475], thanks [@snewcomer])
61+
- [`no-extraneous-dependencies`]: Add `considerInParents` option to support package.json files in parent folders ([#2481], thanks [@luxaritas])
6162

6263
### Fixed
6364
- [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -1091,6 +1092,7 @@ for info on changes for earlier releases.
10911092
[#2506]: https://github.com/import-js/eslint-plugin-import/pull/2506
10921093
[#2503]: https://github.com/import-js/eslint-plugin-import/pull/2503
10931094
[#2490]: https://github.com/import-js/eslint-plugin-import/pull/2490
1095+
[#2481]: https://github.com/import-js/eslint-plugin-import/pull/2481
10941096
[#2475]: https://github.com/import-js/eslint-plugin-import/pull/2475
10951097
[#2473]: https://github.com/import-js/eslint-plugin-import/pull/2473
10961098
[#2466]: https://github.com/import-js/eslint-plugin-import/pull/2466
@@ -1757,6 +1759,7 @@ for info on changes for earlier releases.
17571759
[@ludofischer]: https://github.com/ludofischer
17581760
[@Lukas-Kullmann]: https://github.com/Lukas-Kullmann
17591761
[@lukeapage]: https://github.com/lukeapage
1762+
[@luxaritas]: https://github.com/luxaritas
17601763
[@lydell]: https://github.com/lydell
17611764
[@magarcia]: https://github.com/magarcia
17621765
[@Mairu]: https://github.com/Mairu

docs/rules/no-extraneous-dependencies.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,17 @@ There are 2 boolean options to opt into checking extra imports that are normally
4040
"import/no-extraneous-dependencies": ["error", {"includeInternal": true, "includeTypes": true}]
4141
```
4242

43-
Also there is one more option called `packageDir`, this option is to specify the path to the folder containing package.json.
43+
To take `package.json` files in parent directories into account, use the `considerInParents` option.
44+
This takes an array of strings, which can include `"prod"`, `"dev"`, `"peer"`, `"optional"`, and/or `"bundled"`.
45+
The default is an empty array (only the nearest `package.json` is taken into account).
46+
47+
For example, the following would allow imports when the relevant packages are found in either `dependencies`
48+
or `devDependencies` in either the closest `package.json` or any `package.json` found in parent directories:
49+
```js
50+
"import/no-extraneous-dependencies": ["error", { "considerInParents": ["prod", "dev"] }]
51+
```
52+
53+
To specify the path to the folder containing package.json, use the `packageDir` option.
4454

4555
If provided as a relative path string, will be computed relative to the current working directory at linter execution time. If this is not ideal (does not work with some editor integrations), consider using `__dirname` to provide a path relative to your configuration.
4656

src/rules/no-extraneous-dependencies.js

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import path from 'path';
22
import fs from 'fs';
3-
import pkgUp from 'eslint-module-utils/pkgUp';
43
import minimatch from 'minimatch';
54
import resolve from 'eslint-module-utils/resolve';
65
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
@@ -40,16 +39,43 @@ function extractDepFields(pkg) {
4039
};
4140
}
4241

43-
function getPackageDepFields(packageJsonPath, throwAtRead) {
44-
if (!depFieldCache.has(packageJsonPath)) {
45-
const depFields = extractDepFields(readJSON(packageJsonPath, throwAtRead));
46-
depFieldCache.set(packageJsonPath, depFields);
42+
function getPackageDepFields(packageDir, considerInParents, requireInDir) {
43+
const cacheKey = JSON.stringify({ packageDir, considerInParents: [...considerInParents] });
44+
45+
if (!depFieldCache.has(cacheKey)) {
46+
// try in the current directory, erroring if the user explicitly specified this directory
47+
// and reading fails
48+
const parsedPackage = readJSON(path.join(packageDir, 'package.json'), requireInDir);
49+
const depFields = extractDepFields(parsedPackage || {});
50+
51+
// If readJSON returned nothing, we want to keep searching since the current directory didn't
52+
// have a package.json. Also keep searching if we're merging in some set of parents dependencies.
53+
// However, if we're already at the root, stop.
54+
if ((!parsedPackage || considerInParents.size > 0) && packageDir !== path.parse(packageDir).root) {
55+
const parentDepFields = getPackageDepFields(path.dirname(packageDir), considerInParents, false);
56+
57+
Object.keys(depFields).forEach(depsKey => {
58+
if (
59+
(depsKey === 'dependencies' && considerInParents.has('prod')) ||
60+
(depsKey === 'devDependencies' && considerInParents.has('dev')) ||
61+
(depsKey === 'peerDependencies' && considerInParents.has('peer')) ||
62+
(depsKey === 'optionalDependencies' && considerInParents.has('optional'))
63+
) {
64+
Object.assign(depFields[depsKey], parentDepFields[depsKey]);
65+
}
66+
if (depsKey === 'bundledDependencies' && considerInParents.has('bundled')) {
67+
depFields[depsKey] = depFields[depsKey].concat(parentDepFields[depsKey]);
68+
}
69+
});
70+
}
71+
72+
depFieldCache.set(cacheKey, depFields);
4773
}
4874

49-
return depFieldCache.get(packageJsonPath);
75+
return depFieldCache.get(cacheKey);
5076
}
5177

52-
function getDependencies(context, packageDir) {
78+
function getDependencies(context, packageDir, considerInParents) {
5379
let paths = [];
5480
try {
5581
const packageContent = {
@@ -71,22 +97,24 @@ function getDependencies(context, packageDir) {
7197
if (paths.length > 0) {
7298
// use rule config to find package.json
7399
paths.forEach(dir => {
74-
const packageJsonPath = path.join(dir, 'package.json');
75-
const _packageContent = getPackageDepFields(packageJsonPath, true);
76-
Object.keys(packageContent).forEach(depsKey =>
77-
Object.assign(packageContent[depsKey], _packageContent[depsKey]),
78-
);
100+
const _packageContent = getPackageDepFields(dir, considerInParents, true);
101+
Object.keys(packageContent).forEach(depsKey => {
102+
if (depsKey === 'bundledDependencies') {
103+
packageContent[depsKey] = packageContent[depsKey].concat(_packageContent[depsKey]);
104+
} else {
105+
Object.assign(packageContent[depsKey], _packageContent[depsKey]);
106+
}
107+
});
79108
});
80109
} else {
81-
const packageJsonPath = pkgUp({
82-
cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(),
83-
normalize: false,
84-
});
85-
86110
// use closest package.json
87111
Object.assign(
88112
packageContent,
89-
getPackageDepFields(packageJsonPath, false),
113+
getPackageDepFields(
114+
path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()),
115+
considerInParents,
116+
false,
117+
),
90118
);
91119
}
92120

@@ -275,6 +303,21 @@ module.exports = {
275303
'packageDir': { 'type': ['string', 'array'] },
276304
'includeInternal': { 'type': ['boolean'] },
277305
'includeTypes': { 'type': ['boolean'] },
306+
'considerInParents': {
307+
'type': 'array',
308+
'uniqueItems': true,
309+
'additionalItems': false,
310+
'items': {
311+
'type': 'string',
312+
'enum': [
313+
'prod',
314+
'dev',
315+
'peer',
316+
'bundled',
317+
'optional',
318+
],
319+
},
320+
},
278321
},
279322
'additionalProperties': false,
280323
},
@@ -284,7 +327,7 @@ module.exports = {
284327
create(context) {
285328
const options = context.options[0] || {};
286329
const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
287-
const deps = getDependencies(context, options.packageDir) || extractDepFields({});
330+
const deps = getDependencies(context, options.packageDir, new Set(options.considerInParents || [])) || extractDepFields({});
288331

289332
const depsOptions = {
290333
allowDevDeps: testConfig(options.devDependencies, filename) !== false,

tests/files/monorepo/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,12 @@
55
},
66
"devDependencies": {
77
"left-pad": "^1.2.0"
8-
}
8+
},
9+
"peerDependencies": {
10+
"lodash": "4.17.21"
11+
},
12+
"optionalDependencies": {
13+
"chalk": "5.2.0"
14+
},
15+
"bundledDependencies": ["commander"]
916
}

tests/src/rules/no-extraneous-dependencies.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,30 @@ ruleTester.run('no-extraneous-dependencies', rule, {
116116
code: 'import rightpad from "right-pad";',
117117
options: [{ packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested] }],
118118
}),
119+
test({
120+
code: 'import leftpad from "left-pad";',
121+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod', 'dev'] }],
122+
}),
123+
test({
124+
code: 'import rightpad from "right-pad";',
125+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod', 'dev'] }],
126+
}),
127+
test({
128+
code: 'import leftpad from "left-pad";',
129+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['dev'] }],
130+
}),
131+
test({
132+
code: 'import lodash from "lodash";',
133+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['peer'] }],
134+
}),
135+
test({
136+
code: 'import chalk from "chalk";',
137+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['optional'] }],
138+
}),
139+
test({
140+
code: 'import commander from "commander";',
141+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['bundled'] }],
142+
}),
119143
test({ code: 'import foo from "@generated/foo"' }),
120144
test({
121145
code: 'import foo from "@generated/foo"',
@@ -319,6 +343,48 @@ ruleTester.run('no-extraneous-dependencies', rule, {
319343
message: "'left-pad' should be listed in the project's dependencies. Run 'npm i -S left-pad' to add it",
320344
}],
321345
}),
346+
test({
347+
code: 'import rightpad from "right-pad";',
348+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: [] }],
349+
errors: [{
350+
message: "'right-pad' should be listed in the project's dependencies. Run 'npm i -S right-pad' to add it",
351+
}],
352+
}),
353+
test({
354+
code: 'import rightpad from "right-pad";',
355+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['dev'] }],
356+
errors: [{
357+
message: "'right-pad' should be listed in the project's dependencies. Run 'npm i -S right-pad' to add it",
358+
}],
359+
}),
360+
test({
361+
code: 'import leftpad from "left-pad";',
362+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod'] }],
363+
errors: [{
364+
message: "'left-pad' should be listed in the project's dependencies. Run 'npm i -S left-pad' to add it",
365+
}],
366+
}),
367+
test({
368+
code: 'import lodash from "lodash";',
369+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod'] }],
370+
errors: [{
371+
message: "'lodash' should be listed in the project's dependencies. Run 'npm i -S lodash' to add it",
372+
}],
373+
}),
374+
test({
375+
code: 'import chalk from "chalk";',
376+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod'] }],
377+
errors: [{
378+
message: "'chalk' should be listed in the project's dependencies. Run 'npm i -S chalk' to add it",
379+
}],
380+
}),
381+
test({
382+
code: 'import commander from "commander";',
383+
options: [{ packageDir: packageDirMonoRepoWithNested, considerInParents: ['prod'] }],
384+
errors: [{
385+
message: "'commander' should be listed in the project's dependencies. Run 'npm i -S commander' to add it",
386+
}],
387+
}),
322388
test({
323389
code: 'import react from "react";',
324390
filename: path.join(packageDirMonoRepoRoot, 'foo.js'),

0 commit comments

Comments
 (0)