Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/src/rules/no-extraneous-dependencies.js
829 views
1
import path from 'path';
2
import fs from 'fs';
3
import readPkgUp from 'eslint-module-utils/readPkgUp';
4
import minimatch from 'minimatch';
5
import resolve from 'eslint-module-utils/resolve';
6
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
7
import importType from '../core/importType';
8
import { getFilePackageName } from '../core/packagePath';
9
import docsUrl from '../docsUrl';
10
11
const depFieldCache = new Map();
12
13
function hasKeys(obj = {}) {
14
return Object.keys(obj).length > 0;
15
}
16
17
function arrayOrKeys(arrayOrObject) {
18
return Array.isArray(arrayOrObject) ? arrayOrObject : Object.keys(arrayOrObject);
19
}
20
21
function extractDepFields(pkg) {
22
return {
23
dependencies: pkg.dependencies || {},
24
devDependencies: pkg.devDependencies || {},
25
optionalDependencies: pkg.optionalDependencies || {},
26
peerDependencies: pkg.peerDependencies || {},
27
// BundledDeps should be in the form of an array, but object notation is also supported by
28
// `npm`, so we convert it to an array if it is an object
29
bundledDependencies: arrayOrKeys(pkg.bundleDependencies || pkg.bundledDependencies || []),
30
};
31
}
32
33
function getDependencies(context, packageDir) {
34
let paths = [];
35
try {
36
const packageContent = {
37
dependencies: {},
38
devDependencies: {},
39
optionalDependencies: {},
40
peerDependencies: {},
41
bundledDependencies: [],
42
};
43
44
if (packageDir && packageDir.length > 0) {
45
if (!Array.isArray(packageDir)) {
46
paths = [path.resolve(packageDir)];
47
} else {
48
paths = packageDir.map(dir => path.resolve(dir));
49
}
50
}
51
52
if (paths.length > 0) {
53
// use rule config to find package.json
54
paths.forEach(dir => {
55
const packageJsonPath = path.join(dir, 'package.json');
56
if (!depFieldCache.has(packageJsonPath)) {
57
const depFields = extractDepFields(
58
JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')),
59
);
60
depFieldCache.set(packageJsonPath, depFields);
61
}
62
const _packageContent = depFieldCache.get(packageJsonPath);
63
Object.keys(packageContent).forEach(depsKey =>
64
Object.assign(packageContent[depsKey], _packageContent[depsKey]),
65
);
66
});
67
} else {
68
// use closest package.json
69
Object.assign(
70
packageContent,
71
extractDepFields(
72
readPkgUp({ cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), normalize: false }).pkg,
73
),
74
);
75
}
76
77
if (![
78
packageContent.dependencies,
79
packageContent.devDependencies,
80
packageContent.optionalDependencies,
81
packageContent.peerDependencies,
82
packageContent.bundledDependencies,
83
].some(hasKeys)) {
84
return null;
85
}
86
87
return packageContent;
88
} catch (e) {
89
if (paths.length > 0 && e.code === 'ENOENT') {
90
context.report({
91
message: 'The package.json file could not be found.',
92
loc: { line: 0, column: 0 },
93
});
94
}
95
if (e.name === 'JSONError' || e instanceof SyntaxError) {
96
context.report({
97
message: 'The package.json file could not be parsed: ' + e.message,
98
loc: { line: 0, column: 0 },
99
});
100
}
101
102
return null;
103
}
104
}
105
106
function missingErrorMessage(packageName) {
107
return `'${packageName}' should be listed in the project's dependencies. ` +
108
`Run 'npm i -S ${packageName}' to add it`;
109
}
110
111
function devDepErrorMessage(packageName) {
112
return `'${packageName}' should be listed in the project's dependencies, not devDependencies.`;
113
}
114
115
function optDepErrorMessage(packageName) {
116
return `'${packageName}' should be listed in the project's dependencies, ` +
117
`not optionalDependencies.`;
118
}
119
120
function getModuleOriginalName(name) {
121
const [first, second] = name.split('/');
122
return first.startsWith('@') ? `${first}/${second}` : first;
123
}
124
125
function getModuleRealName(resolved) {
126
return getFilePackageName(resolved);
127
}
128
129
function checkDependencyDeclaration(deps, packageName, declarationStatus) {
130
const newDeclarationStatus = declarationStatus || {
131
isInDeps: false,
132
isInDevDeps: false,
133
isInOptDeps: false,
134
isInPeerDeps: false,
135
isInBundledDeps: false,
136
};
137
138
// in case of sub package.json inside a module
139
// check the dependencies on all hierarchy
140
const packageHierarchy = [];
141
const packageNameParts = packageName ? packageName.split('/') : [];
142
packageNameParts.forEach((namePart, index) => {
143
if (!namePart.startsWith('@')) {
144
const ancestor = packageNameParts.slice(0, index + 1).join('/');
145
packageHierarchy.push(ancestor);
146
}
147
});
148
149
return packageHierarchy.reduce((result, ancestorName) => {
150
return {
151
isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined,
152
isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined,
153
isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined,
154
isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined,
155
isInBundledDeps:
156
result.isInBundledDeps || deps.bundledDependencies.indexOf(ancestorName) !== -1,
157
};
158
}, newDeclarationStatus);
159
}
160
161
function reportIfMissing(context, deps, depsOptions, node, name) {
162
// Do not report when importing types
163
if (
164
node.importKind === 'type' ||
165
node.importKind === 'typeof'
166
) {
167
return;
168
}
169
170
if (importType(name, context) !== 'external') {
171
return;
172
}
173
174
const resolved = resolve(name, context);
175
if (!resolved) { return; }
176
177
const importPackageName = getModuleOriginalName(name);
178
let declarationStatus = checkDependencyDeclaration(deps, importPackageName);
179
180
if (
181
declarationStatus.isInDeps ||
182
(depsOptions.allowDevDeps && declarationStatus.isInDevDeps) ||
183
(depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) ||
184
(depsOptions.allowOptDeps && declarationStatus.isInOptDeps) ||
185
(depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)
186
) {
187
return;
188
}
189
190
// test the real name from the resolved package.json
191
// if not aliased imports (alias/react for example), importPackageName can be misinterpreted
192
const realPackageName = getModuleRealName(resolved);
193
if (realPackageName && realPackageName !== importPackageName) {
194
declarationStatus = checkDependencyDeclaration(deps, realPackageName, declarationStatus);
195
196
if (
197
declarationStatus.isInDeps ||
198
(depsOptions.allowDevDeps && declarationStatus.isInDevDeps) ||
199
(depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) ||
200
(depsOptions.allowOptDeps && declarationStatus.isInOptDeps) ||
201
(depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)
202
) {
203
return;
204
}
205
}
206
207
if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) {
208
context.report(node, devDepErrorMessage(realPackageName || importPackageName));
209
return;
210
}
211
212
if (declarationStatus.isInOptDeps && !depsOptions.allowOptDeps) {
213
context.report(node, optDepErrorMessage(realPackageName || importPackageName));
214
return;
215
}
216
217
context.report(node, missingErrorMessage(realPackageName || importPackageName));
218
}
219
220
function testConfig(config, filename) {
221
// Simplest configuration first, either a boolean or nothing.
222
if (typeof config === 'boolean' || typeof config === 'undefined') {
223
return config;
224
}
225
// Array of globs.
226
return config.some(c => (
227
minimatch(filename, c) ||
228
minimatch(filename, path.join(process.cwd(), c))
229
));
230
}
231
232
module.exports = {
233
meta: {
234
type: 'problem',
235
docs: {
236
url: docsUrl('no-extraneous-dependencies'),
237
},
238
239
schema: [
240
{
241
'type': 'object',
242
'properties': {
243
'devDependencies': { 'type': ['boolean', 'array'] },
244
'optionalDependencies': { 'type': ['boolean', 'array'] },
245
'peerDependencies': { 'type': ['boolean', 'array'] },
246
'bundledDependencies': { 'type': ['boolean', 'array'] },
247
'packageDir': { 'type': ['string', 'array'] },
248
},
249
'additionalProperties': false,
250
},
251
],
252
},
253
254
create(context) {
255
const options = context.options[0] || {};
256
const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
257
const deps = getDependencies(context, options.packageDir) || extractDepFields({});
258
259
const depsOptions = {
260
allowDevDeps: testConfig(options.devDependencies, filename) !== false,
261
allowOptDeps: testConfig(options.optionalDependencies, filename) !== false,
262
allowPeerDeps: testConfig(options.peerDependencies, filename) !== false,
263
allowBundledDeps: testConfig(options.bundledDependencies, filename) !== false,
264
};
265
266
return moduleVisitor((source, node) => {
267
reportIfMissing(context, deps, depsOptions, node, source.value);
268
}, { commonjs: true });
269
},
270
};
271
272