eslint-plugin-n 18.0.1 → 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Additional ESLint rules for Node.js
|
|
|
10
10
|
|
|
11
11
|
## 🎨 Playground
|
|
12
12
|
|
|
13
|
-
[online-playground](https://eslint-online-playground.netlify.app/#
|
|
13
|
+
[online-playground](https://eslint-online-playground.netlify.app/#eNptjzFuwzAMRa8icGqB2NndtbcoOxgybaiVSUGSgxSG717KUoAMWSiReJ//c4cU7ZXu4xo89T8JBrDCSbTxsryFKJZS6olv7x/IcAFK3nHuFZrdUgVuDRKzYTNHWQ02pAt+Wxx3jKBKZLqf1ETzuPlsvpCN2UsxppJpMLsuOS51GDdPZVQ7o3v5ytK1RJ0mQhiKW4wSESp2lEfLdw0bRvs7LuUuYQ167kLIf4GqdpVJXRBOS4SJbp8UiCdi6ygVptk/jqoyP2ZK+m9JX1z8TLVIBxz/MqF45g==)
|
|
14
14
|
|
|
15
15
|
## 💿 Install & Usage
|
|
16
16
|
|
|
@@ -34,8 +34,10 @@ import node from "eslint-plugin-n"
|
|
|
34
34
|
import {defineConfig} from "eslint/config"
|
|
35
35
|
|
|
36
36
|
export default defineConfig([
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
{
|
|
38
|
+
plugins: {n: node},
|
|
39
|
+
extends: ["n/recommended-module"],
|
|
40
|
+
}
|
|
39
41
|
])
|
|
40
42
|
```
|
|
41
43
|
|
|
@@ -46,10 +48,12 @@ import node from "eslint-plugin-n"
|
|
|
46
48
|
import {defineConfig} from "eslint/config"
|
|
47
49
|
|
|
48
50
|
export default defineConfig([
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
{
|
|
52
|
+
plugins: {n: node},
|
|
53
|
+
rules: {
|
|
54
|
+
"n/no-unsupported-features/es-builtins": "error",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
53
57
|
])
|
|
54
58
|
```
|
|
55
59
|
|
|
@@ -73,9 +77,10 @@ The rules get the supported Node.js version range from the following, falling ba
|
|
|
73
77
|
1. Rule configuration `version`
|
|
74
78
|
2. ESLint [shared setting](http://eslint.org/docs/user-guide/configuring.html#adding-shared-settings) `node.version`
|
|
75
79
|
3. `package.json` [`engines`] field
|
|
76
|
-
4.
|
|
80
|
+
4. `package.json` [`devEngines.runtime`](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#devengines) field (when `name` is `"node"`)
|
|
81
|
+
5. `>=16.0.0`
|
|
77
82
|
|
|
78
|
-
If you omit the [engines] field, this rule chooses `>=16.0.0` as the configured Node.js version since `16` is the maintained lts (see also [Node.js Release Working Group](https://github.com/nodejs/Release#readme)).
|
|
83
|
+
If you omit both the [engines] field and the `devEngines.runtime` field (with `name` set to `"node"`), this rule chooses `>=16.0.0` as the configured Node.js version since `16` is the maintained lts (see also [Node.js Release Working Group](https://github.com/nodejs/Release#readme)).
|
|
79
84
|
|
|
80
85
|
For Node.js packages, using the [`engines`] field is recommended because it's the official way to indicate support:
|
|
81
86
|
|
|
@@ -188,8 +193,10 @@ import node from "eslint-plugin-n"
|
|
|
188
193
|
import {defineConfig} from "eslint/config"
|
|
189
194
|
|
|
190
195
|
export default defineConfig([
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
{
|
|
197
|
+
plugins: {n: node},
|
|
198
|
+
extends: ["n/mixed-esm-and-cjs"],
|
|
199
|
+
},
|
|
193
200
|
])
|
|
194
201
|
```
|
|
195
202
|
|
|
@@ -3,9 +3,220 @@
|
|
|
3
3
|
* See LICENSE file in root directory for full license.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import globrex from "globrex"
|
|
8
|
+
import { Cache } from "./cache.js"
|
|
6
9
|
import { getAllowModules } from "./get-allow-modules.js"
|
|
7
10
|
import { getPackageJson } from "./get-package-json.js"
|
|
8
11
|
|
|
12
|
+
const workspacePackageJsonsCache = new Cache()
|
|
13
|
+
const workspacePatternMatcherCache = new Cache()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} workspaces - The package.json workspaces value.
|
|
17
|
+
* @returns {string[]} Workspace package patterns.
|
|
18
|
+
*/
|
|
19
|
+
function getWorkspacePatterns(workspaces) {
|
|
20
|
+
if (Array.isArray(workspaces)) {
|
|
21
|
+
return workspaces.map(String)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
workspaces != null &&
|
|
26
|
+
typeof workspaces === "object" &&
|
|
27
|
+
"packages" in workspaces &&
|
|
28
|
+
Array.isArray(workspaces.packages)
|
|
29
|
+
) {
|
|
30
|
+
return workspaces.packages.map(String)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} pattern - A package.json workspace pattern.
|
|
38
|
+
* @returns {string} A pattern comparable to path.posix-style relative paths.
|
|
39
|
+
*/
|
|
40
|
+
function normalizeWorkspacePattern(pattern) {
|
|
41
|
+
return pattern
|
|
42
|
+
.replace(/\\/gu, "/")
|
|
43
|
+
.replace(/^\.\//u, "")
|
|
44
|
+
.replace(/\/+$/u, "")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} relativePath - A relative package directory path.
|
|
49
|
+
* @param {string[]} patterns - Workspace package patterns.
|
|
50
|
+
* @returns {boolean} Whether the path is included by the patterns.
|
|
51
|
+
*/
|
|
52
|
+
function matchesWorkspacePattern(relativePath, patterns) {
|
|
53
|
+
let matched = false
|
|
54
|
+
|
|
55
|
+
for (const rawPattern of patterns) {
|
|
56
|
+
const negated = rawPattern.startsWith("!")
|
|
57
|
+
const pattern = normalizeWorkspacePattern(
|
|
58
|
+
negated ? rawPattern.slice(1) : rawPattern
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (pattern === "") {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (getWorkspacePatternMatcher(pattern).test(relativePath)) {
|
|
66
|
+
if (negated) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
matched = true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return matched
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} pattern - A normalized package.json workspace pattern.
|
|
79
|
+
* @returns {RegExp} A matcher for package paths.
|
|
80
|
+
*/
|
|
81
|
+
function getWorkspacePatternMatcher(pattern) {
|
|
82
|
+
let matcher = workspacePatternMatcherCache.get(pattern)
|
|
83
|
+
|
|
84
|
+
if (matcher == null) {
|
|
85
|
+
matcher = globrex(pattern, {
|
|
86
|
+
extended: true,
|
|
87
|
+
globstar: true,
|
|
88
|
+
}).regex
|
|
89
|
+
workspacePatternMatcherCache.set(pattern, matcher)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return matcher
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} dir - A directory path.
|
|
97
|
+
* @returns {import('type-fest').JsonObject|null} The package.json in the directory, if present.
|
|
98
|
+
*/
|
|
99
|
+
function getPackageJsonInDirectory(dir) {
|
|
100
|
+
const packageInfo = getPackageJson(path.join(dir, "__placeholder__.js"))
|
|
101
|
+
const filePath = path.join(dir, "package.json")
|
|
102
|
+
|
|
103
|
+
return packageInfo != null && packageInfo.filePath === filePath
|
|
104
|
+
? packageInfo
|
|
105
|
+
: null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {import('type-fest').JsonObject} packageInfo - A package.json object.
|
|
110
|
+
* @param {import('type-fest').JsonObject} workspacePackageInfo - An ancestor package.json object.
|
|
111
|
+
* @returns {boolean} Whether the package is in the ancestor's workspace.
|
|
112
|
+
*/
|
|
113
|
+
function isWorkspacePackage(packageInfo, workspacePackageInfo) {
|
|
114
|
+
if (
|
|
115
|
+
typeof packageInfo.filePath !== "string" ||
|
|
116
|
+
typeof workspacePackageInfo.filePath !== "string"
|
|
117
|
+
) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const packageDir = path.dirname(packageInfo.filePath)
|
|
122
|
+
const workspaceDir = path.dirname(workspacePackageInfo.filePath)
|
|
123
|
+
const relativePath = path
|
|
124
|
+
.relative(workspaceDir, packageDir)
|
|
125
|
+
.replace(/\\/gu, "/")
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
relativePath === "" ||
|
|
129
|
+
relativePath === ".." ||
|
|
130
|
+
relativePath.startsWith("../") ||
|
|
131
|
+
path.isAbsolute(relativePath)
|
|
132
|
+
) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return matchesWorkspacePattern(
|
|
137
|
+
relativePath,
|
|
138
|
+
getWorkspacePatterns(workspacePackageInfo.workspaces)
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {import('type-fest').JsonObject} packageInfo - A package.json object.
|
|
144
|
+
* @returns {import('type-fest').JsonObject[]} Matching workspace root package.json objects.
|
|
145
|
+
*/
|
|
146
|
+
function getWorkspacePackageJsons(packageInfo) {
|
|
147
|
+
if (typeof packageInfo.filePath !== "string") {
|
|
148
|
+
return []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cached = workspacePackageJsonsCache.get(packageInfo.filePath)
|
|
152
|
+
if (cached != null) {
|
|
153
|
+
return cached
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const workspacePackageJsons = []
|
|
157
|
+
let dir = path.resolve(path.dirname(packageInfo.filePath), "..")
|
|
158
|
+
let prevDir = ""
|
|
159
|
+
|
|
160
|
+
do {
|
|
161
|
+
const workspacePackageInfo = getPackageJsonInDirectory(dir)
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
workspacePackageInfo != null &&
|
|
165
|
+
isWorkspacePackage(packageInfo, workspacePackageInfo)
|
|
166
|
+
) {
|
|
167
|
+
workspacePackageJsons.push(workspacePackageInfo)
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
prevDir = dir
|
|
172
|
+
dir = path.resolve(dir, "..")
|
|
173
|
+
} while (dir !== prevDir)
|
|
174
|
+
|
|
175
|
+
workspacePackageJsonsCache.set(packageInfo.filePath, workspacePackageJsons)
|
|
176
|
+
return workspacePackageJsons
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {import('type-fest').JsonObject} packageInfo - A package.json object.
|
|
181
|
+
* @returns {string[]} Package and dependency names.
|
|
182
|
+
*/
|
|
183
|
+
function getPackageNames(packageInfo) {
|
|
184
|
+
return (
|
|
185
|
+
typeof packageInfo.name === "string" ? [packageInfo.name] : []
|
|
186
|
+
).concat(
|
|
187
|
+
getDependencyNames(packageInfo.dependencies),
|
|
188
|
+
getDependencyNames(packageInfo.devDependencies),
|
|
189
|
+
getDependencyNames(packageInfo.peerDependencies),
|
|
190
|
+
getDependencyNames(packageInfo.optionalDependencies)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {unknown} dependencies - A package.json dependency object.
|
|
196
|
+
* @returns {string[]} Dependency names.
|
|
197
|
+
*/
|
|
198
|
+
function getDependencyNames(dependencies) {
|
|
199
|
+
return dependencies != null &&
|
|
200
|
+
typeof dependencies === "object" &&
|
|
201
|
+
Array.isArray(dependencies) === false
|
|
202
|
+
? Object.keys(dependencies)
|
|
203
|
+
: []
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the matching DefinitelyTyped package name for a module.
|
|
208
|
+
* @param {string} moduleName - An npm module name.
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function getTypesPackageName(moduleName) {
|
|
212
|
+
if (moduleName.startsWith("@")) {
|
|
213
|
+
const [scope, name] = moduleName.slice(1).split("/")
|
|
214
|
+
return `@types/${scope}__${name}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return `@types/${moduleName}`
|
|
218
|
+
}
|
|
219
|
+
|
|
9
220
|
/**
|
|
10
221
|
* Checks whether or not each requirement target is published via package.json.
|
|
11
222
|
*
|
|
@@ -24,11 +235,8 @@ export function checkExtraneous(context, filePath, targets) {
|
|
|
24
235
|
|
|
25
236
|
const allowed = new Set(getAllowModules(context))
|
|
26
237
|
const dependencies = new Set(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Object.keys(packageInfo.devDependencies || {}),
|
|
30
|
-
Object.keys(packageInfo.peerDependencies || {}),
|
|
31
|
-
Object.keys(packageInfo.optionalDependencies || {})
|
|
238
|
+
getPackageNames(packageInfo).concat(
|
|
239
|
+
getWorkspacePackageJsons(packageInfo).flatMap(getPackageNames)
|
|
32
240
|
)
|
|
33
241
|
)
|
|
34
242
|
|
|
@@ -37,6 +245,10 @@ export function checkExtraneous(context, filePath, targets) {
|
|
|
37
245
|
target.moduleName != null &&
|
|
38
246
|
target.filePath != null &&
|
|
39
247
|
!dependencies.has(target.moduleName) &&
|
|
248
|
+
!(
|
|
249
|
+
target.moduleStyle === "type" &&
|
|
250
|
+
dependencies.has(getTypesPackageName(target.moduleName))
|
|
251
|
+
) &&
|
|
40
252
|
!allowed.has(target.moduleName) &&
|
|
41
253
|
// https://github.com/eslint-community/eslint-plugin-n/issues/379
|
|
42
254
|
!target.hasTSAlias()
|
|
@@ -27,6 +27,9 @@ function getVersionRange(option) {
|
|
|
27
27
|
/**
|
|
28
28
|
* @typedef {{ [EngineName in 'npm' | 'node' | string]?: string }} Engines
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {{ name?: string, version?: string, onFail?: string }} DevEngineEntry
|
|
32
|
+
*/
|
|
30
33
|
/**
|
|
31
34
|
* Get the `engines.node` field of package.json.
|
|
32
35
|
* @param {import('eslint').Rule.RuleContext} context The path to the current linting file.
|
|
@@ -45,12 +48,48 @@ function getEnginesNode(context) {
|
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Get the `devEngines.runtime` (`name: "node"`) version from package.json.
|
|
53
|
+
* `devEngines.runtime` may be a single object or an array of objects.
|
|
54
|
+
* See https://docs.npmjs.com/cli/v11/configuring-npm/package-json#devengines
|
|
55
|
+
* @param {import('eslint').Rule.RuleContext} context The path to the current linting file.
|
|
56
|
+
* @returns {import("semver").Range | undefined} The range object of the matching `devEngines.runtime.version`.
|
|
57
|
+
*/
|
|
58
|
+
function getDevEnginesNode(context) {
|
|
59
|
+
const filename = context.filename
|
|
60
|
+
const info = getPackageJson(filename)
|
|
61
|
+
if (
|
|
62
|
+
info?.devEngines == null ||
|
|
63
|
+
typeof info.devEngines !== "object" ||
|
|
64
|
+
Array.isArray(info.devEngines) ||
|
|
65
|
+
!("runtime" in info.devEngines) ||
|
|
66
|
+
info.devEngines.runtime == null
|
|
67
|
+
) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const runtime = info.devEngines.runtime
|
|
71
|
+
const entries = /** @type {DevEngineEntry[]} */ (
|
|
72
|
+
Array.isArray(runtime) ? runtime : [runtime]
|
|
73
|
+
)
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (
|
|
76
|
+
entry != null &&
|
|
77
|
+
typeof entry === "object" &&
|
|
78
|
+
entry.name === "node" &&
|
|
79
|
+
typeof entry.version === "string"
|
|
80
|
+
) {
|
|
81
|
+
return getSemverRange(entry.version)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
/**
|
|
49
87
|
* Gets version configuration.
|
|
50
88
|
*
|
|
51
89
|
* 1. Parse a given version then return it if it's valid.
|
|
52
90
|
* 2. Look package.json up and parse `engines.node` then return it if it's valid.
|
|
53
|
-
* 3.
|
|
91
|
+
* 3. Look package.json up and parse `devEngines.runtime` (`name: "node"`) then return it if it's valid.
|
|
92
|
+
* 4. Return `>=16.0.0`.
|
|
54
93
|
*
|
|
55
94
|
* @param {import('eslint').Rule.RuleContext} context The version range text.
|
|
56
95
|
* This will be used to look package.json up if `version` is not a valid version range.
|
|
@@ -64,6 +103,7 @@ export function getConfiguredNodeVersion(context) {
|
|
|
64
103
|
/** @type {VersionOption} */ (context.settings?.node)
|
|
65
104
|
) ??
|
|
66
105
|
getEnginesNode(context) ??
|
|
106
|
+
getDevEnginesNode(context) ??
|
|
67
107
|
fallbackRange
|
|
68
108
|
)
|
|
69
109
|
}
|
|
@@ -102,7 +102,7 @@ function parseWhiteList(files) {
|
|
|
102
102
|
for (const file of files) {
|
|
103
103
|
if (typeof file === "string" && file) {
|
|
104
104
|
const body = path.posix
|
|
105
|
-
.normalize(file.replace(
|
|
105
|
+
.normalize(file.replace(/^[!/]/u, ""))
|
|
106
106
|
.replace(/\/+$/u, "")
|
|
107
107
|
|
|
108
108
|
if (file.startsWith("!")) {
|
|
@@ -12,8 +12,6 @@ const cache = new Cache()
|
|
|
12
12
|
/**
|
|
13
13
|
* Reads the `package.json` data in a given path.
|
|
14
14
|
*
|
|
15
|
-
* Don't cache the data.
|
|
16
|
-
*
|
|
17
15
|
* @param {string} dir - The path to a directory to read.
|
|
18
16
|
* @returns {import('type-fest').JsonObject|null} The read `package.json` data, or null.
|
|
19
17
|
*/
|
|
@@ -232,6 +232,15 @@ export class ImportTarget {
|
|
|
232
232
|
: "import"
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
if (
|
|
236
|
+
(node.parent.type === "ExportAllDeclaration" ||
|
|
237
|
+
node.parent.type === "ExportNamedDeclaration") &&
|
|
238
|
+
"exportKind" in node.parent &&
|
|
239
|
+
node.parent.exportKind === "type"
|
|
240
|
+
) {
|
|
241
|
+
return "type"
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
node = node.parent
|
|
236
245
|
} while (node.parent)
|
|
237
246
|
|