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/#eNp1jzEOwjAMRa9SeWFpYS8rOwtiIQxRalWBxIniFIFQ747bCASV2BK/Lz//J3AyG7xrHx2uLwwtWB9DytXKW2ZLfVP+q60iqGGN7CzlZCQbSNJPRVWlAO8ZqWMFbXWS3xxqE5rgvcyxU1BLKrqht9TS5oec67Kj0PcO+gI6MaZ9zDYUPEtnrfH6iIkFTHMFTmfkLLZ3gsOQDB4eEUvAh25w8p74qGiUTlGbq+6n9t+NOrztME4nkrG40M79/hgLbEqbZnHXRzu+APYwfks=)
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
- plugins: {n: node},
38
- extends: ["n/recommended-module"],
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
- plugins: {n: node},
50
- rules: {
51
- "n/no-unsupported-features/es-builtins": "error",
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. `>=16.0.0`
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
- plugins: {n: node},
192
- extends: ["n/mixed-esm-and-cjs"],
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
- [packageInfo.name].concat(
28
- Object.keys(packageInfo.dependencies || {}),
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. Return `>=16.0.0`.
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(/^!/u, ""))
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-n",
3
- "version": "18.0.1",
3
+ "version": "18.1.0",
4
4
  "description": "Additional ESLint's rules for Node.js",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || ^22.13.0 || >=24"