eslint-plugin-n 17.23.1 → 17.24.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
@@ -165,9 +165,11 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
165
165
  | [no-unsupported-features/node-builtins](docs/rules/no-unsupported-features/node-builtins.md) | disallow unsupported Node.js built-in APIs on the specified version | 🟢 ✅ | | |
166
166
  | [prefer-global/buffer](docs/rules/prefer-global/buffer.md) | enforce either `Buffer` or `require("buffer").Buffer` | | | |
167
167
  | [prefer-global/console](docs/rules/prefer-global/console.md) | enforce either `console` or `require("console")` | | | |
168
+ | [prefer-global/crypto](docs/rules/prefer-global/crypto.md) | enforce either `crypto` or `require("crypto").webcrypto` | | | |
168
169
  | [prefer-global/process](docs/rules/prefer-global/process.md) | enforce either `process` or `require("process")` | | | |
169
170
  | [prefer-global/text-decoder](docs/rules/prefer-global/text-decoder.md) | enforce either `TextDecoder` or `require("util").TextDecoder` | | | |
170
171
  | [prefer-global/text-encoder](docs/rules/prefer-global/text-encoder.md) | enforce either `TextEncoder` or `require("util").TextEncoder` | | | |
172
+ | [prefer-global/timers](docs/rules/prefer-global/timers.md) | enforce either global timer functions or `require("timers")` | | | |
171
173
  | [prefer-global/url](docs/rules/prefer-global/url.md) | enforce either `URL` or `require("url").URL` | | | |
172
174
  | [prefer-global/url-search-params](docs/rules/prefer-global/url-search-params.md) | enforce either `URLSearchParams` or `require("url").URLSearchParams` | | | |
173
175
  | [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | enforce using the `node:` protocol when importing Node.js builtin modules. | | 🔧 | |
package/lib/all-rules.js CHANGED
@@ -38,11 +38,13 @@ module.exports = {
38
38
  "no-unsupported-features/node-builtins": require("./rules/no-unsupported-features/node-builtins"),
39
39
  "prefer-global/buffer": require("./rules/prefer-global/buffer"),
40
40
  "prefer-global/console": require("./rules/prefer-global/console"),
41
+ "prefer-global/crypto": require("./rules/prefer-global/crypto"),
41
42
  "prefer-global/process": require("./rules/prefer-global/process"),
42
43
  "prefer-global/text-decoder": require("./rules/prefer-global/text-decoder"),
43
44
  "prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"),
44
45
  "prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"),
45
46
  "prefer-global/url": require("./rules/prefer-global/url"),
47
+ "prefer-global/timers": require("./rules/prefer-global/timers"),
46
48
  "prefer-node-protocol": require("./rules/prefer-node-protocol"),
47
49
  "prefer-promises/dns": require("./rules/prefer-promises/dns"),
48
50
  "prefer-promises/fs": require("./rules/prefer-promises/fs"),
@@ -7,17 +7,21 @@
7
7
  const path = require("path")
8
8
  const fs = require("fs")
9
9
  const { convertTsExtensionToJs } = require("../util/map-typescript-extension")
10
+ const getTryExtensions = require("../util/get-try-extensions")
10
11
  const visitImport = require("../util/visit-import")
11
12
 
12
13
  /**
13
14
  * Get all file extensions of the files which have the same basename.
14
15
  * @param {string} filePath The path to the original file to check.
16
+ * @param {string} [basename] Optional basename to use instead of deriving from filePath.
15
17
  * @returns {string[]} File extensions.
16
18
  */
17
- function getExistingExtensions(filePath) {
19
+ function getExistingExtensions(filePath, basename) {
18
20
  const directory = path.dirname(filePath)
19
- const extension = path.extname(filePath)
20
- const basename = path.basename(filePath, extension)
21
+ if (basename == null) {
22
+ const extension = path.extname(filePath)
23
+ basename = path.basename(filePath, extension)
24
+ }
21
25
 
22
26
  try {
23
27
  return fs
@@ -29,6 +33,34 @@ function getExistingExtensions(filePath) {
29
33
  }
30
34
  }
31
35
 
36
+ /**
37
+ * Get the extension of an index file in the given directory.
38
+ * @param {string} directoryPath The directory path to check.
39
+ * @param {string[]} tryExtensions Ordered extension preferences.
40
+ * @returns {string | null} The index file extension or null if none.
41
+ */
42
+ function getIndexExtension(directoryPath, tryExtensions) {
43
+ try {
44
+ if (fs.statSync(directoryPath).isDirectory() === false) {
45
+ return null
46
+ }
47
+ } catch {
48
+ return null
49
+ }
50
+
51
+ const existing = getExistingExtensions(path.join(directoryPath, "index"))
52
+ if (existing.length === 0) {
53
+ return null
54
+ }
55
+
56
+ const preferred = tryExtensions.find(ext => existing.includes(ext))
57
+ if (preferred) {
58
+ return preferred
59
+ }
60
+
61
+ return existing[0] || null
62
+ }
63
+
32
64
  /**
33
65
  * @typedef {[
34
66
  * ("always" | "never")?,
@@ -69,6 +101,7 @@ module.exports = {
69
101
  }
70
102
  const defaultStyle = context.options[0] || "always"
71
103
  const overrideStyle = context.options[1] || {}
104
+ const tryExtensions = getTryExtensions(context, 1)
72
105
 
73
106
  /**
74
107
  * @param {import("../util/import-target.js")} target
@@ -85,15 +118,74 @@ module.exports = {
85
118
 
86
119
  // Get extension.
87
120
  const currentExt = path.extname(name)
88
- const actualExt = path.extname(filePath)
121
+ let actualExt = path.extname(filePath)
122
+ let actualFilePath = filePath
123
+
124
+ // If the file doesn't exist (or is a directory), the resolver may have returned
125
+ // a fallback path. In this case, path.extname may return a "fake" extension
126
+ // (e.g., ".client" for "utils.client" when the actual file is "utils.client.ts").
127
+ // We need to search for the actual file using the full basename from the import.
128
+ const fileExists =
129
+ fs.existsSync(filePath) && fs.statSync(filePath).isFile()
130
+ if (actualExt !== "" && !fileExists) {
131
+ // Use the full basename (e.g., "utils.client") since what path.extname
132
+ // thinks is an extension (e.g., ".client") may not be a real file extension
133
+ const importBasename = path.basename(name)
134
+ const extensions = getExistingExtensions(
135
+ filePath,
136
+ importBasename
137
+ )
138
+ // Find the preferred extension based on tryExtensions order
139
+ const preferred = tryExtensions.find(ext =>
140
+ extensions.includes(ext)
141
+ )
142
+ const foundExt = preferred ?? extensions[0]
143
+ if (foundExt) {
144
+ actualExt = foundExt
145
+ actualFilePath = path.join(
146
+ path.dirname(filePath),
147
+ `${importBasename}${actualExt}`
148
+ )
149
+ }
150
+ }
151
+
152
+ let isDirectoryImport = false
153
+
154
+ // Check for directory imports. This handles both:
155
+ // 1. Normal case: "./my-folder" -> "./my-folder/index.js"
156
+ // 2. Dot-in-name case: "./my-things.client" -> "./my-things.client/index.js"
157
+ // where path.extname incorrectly sees ".client" as an extension
158
+ const isDirectory =
159
+ fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
160
+ if (isDirectory) {
161
+ const indexExt = getIndexExtension(filePath, tryExtensions)
162
+ if (indexExt) {
163
+ isDirectoryImport = true
164
+ actualExt = indexExt
165
+ actualFilePath = path.join(filePath, `index${indexExt}`)
166
+ }
167
+ }
168
+
89
169
  const style = overrideStyle[actualExt] || defaultStyle
90
170
 
91
- const expectedExt = convertTsExtensionToJs(
171
+ let expectedExt = convertTsExtensionToJs(
92
172
  context,
93
- filePath,
173
+ actualFilePath,
94
174
  actualExt
95
175
  )
96
176
 
177
+ if (currentExt === "" && actualExt === "" && !isDirectoryImport) {
178
+ const indexExt = getIndexExtension(filePath, tryExtensions)
179
+ if (indexExt) {
180
+ isDirectoryImport = true
181
+ expectedExt = convertTsExtensionToJs(
182
+ context,
183
+ path.join(filePath, `index${indexExt}`),
184
+ indexExt
185
+ )
186
+ }
187
+ }
188
+
97
189
  // Verify.
98
190
  if (style === "always" && currentExt !== expectedExt) {
99
191
  context.report({
@@ -106,6 +198,13 @@ module.exports = {
106
198
  }
107
199
 
108
200
  const index = node.range[1] - 1
201
+ if (isDirectoryImport) {
202
+ const needsSlash = /[/\\]$/.test(name) ? "" : "/"
203
+ return fixer.insertTextBeforeRange(
204
+ [index, index],
205
+ `${needsSlash}index${expectedExt}`
206
+ )
207
+ }
109
208
  return fixer.insertTextBeforeRange(
110
209
  [index, index],
111
210
  expectedExt
@@ -120,6 +120,9 @@ module.exports = {
120
120
  ":function:exit"() {
121
121
  functionDepth--
122
122
  },
123
+ /**
124
+ * @param {import('estree').AwaitExpression | import('estree').ForOfStatement | import('estree').VariableDeclaration} node
125
+ */
123
126
  "AwaitExpression, ForOfStatement[await=true], VariableDeclaration[kind='await using']"(
124
127
  node
125
128
  ) {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @author Pixel998
3
+ * See LICENSE file in root directory for full license.
4
+ */
5
+ "use strict"
6
+
7
+ const { READ } = require("@eslint-community/eslint-utils")
8
+ const checkForPreferGlobal = require("../../util/check-prefer-global")
9
+
10
+ const traceMap = {
11
+ globals: {
12
+ crypto: { [READ]: true },
13
+ },
14
+ modules: {
15
+ crypto: { webcrypto: { [READ]: true } },
16
+ "node:crypto": { webcrypto: { [READ]: true } },
17
+ },
18
+ }
19
+
20
+ /** @type {import('../rule-module').RuleModule} */
21
+ module.exports = {
22
+ meta: {
23
+ docs: {
24
+ description:
25
+ 'enforce either `crypto` or `require("crypto").webcrypto`',
26
+ recommended: false,
27
+ url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/crypto.md",
28
+ },
29
+ type: "suggestion",
30
+ fixable: null,
31
+ schema: [{ enum: ["always", "never"] }],
32
+ messages: {
33
+ preferGlobal:
34
+ "Unexpected use of 'require(\"crypto\").webcrypto'. Use the global variable 'crypto' instead.",
35
+ preferModule:
36
+ "Unexpected use of the global variable 'crypto'. Use 'require(\"crypto\").webcrypto' instead.",
37
+ },
38
+ },
39
+
40
+ create(context) {
41
+ return {
42
+ "Program:exit"() {
43
+ checkForPreferGlobal(context, traceMap)
44
+ },
45
+ }
46
+ },
47
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @author Pixel998
3
+ * See LICENSE file in root directory for full license.
4
+ */
5
+ "use strict"
6
+
7
+ const { READ } = require("@eslint-community/eslint-utils")
8
+ const checkForPreferGlobal = require("../../util/check-prefer-global")
9
+
10
+ const traceMap = {
11
+ globals: {
12
+ clearImmediate: { [READ]: true },
13
+ clearInterval: { [READ]: true },
14
+ clearTimeout: { [READ]: true },
15
+ setImmediate: { [READ]: true },
16
+ setInterval: { [READ]: true },
17
+ setTimeout: { [READ]: true },
18
+ },
19
+ modules: {
20
+ timers: {
21
+ clearImmediate: { [READ]: true },
22
+ clearInterval: { [READ]: true },
23
+ clearTimeout: { [READ]: true },
24
+ setImmediate: { [READ]: true },
25
+ setInterval: { [READ]: true },
26
+ setTimeout: { [READ]: true },
27
+ },
28
+ "node:timers": {
29
+ clearImmediate: { [READ]: true },
30
+ clearInterval: { [READ]: true },
31
+ clearTimeout: { [READ]: true },
32
+ setImmediate: { [READ]: true },
33
+ setInterval: { [READ]: true },
34
+ setTimeout: { [READ]: true },
35
+ },
36
+ },
37
+ }
38
+
39
+ /** @type {import('../rule-module').RuleModule} */
40
+ module.exports = {
41
+ meta: {
42
+ docs: {
43
+ description:
44
+ 'enforce either global timer functions or `require("timers")`',
45
+ recommended: false,
46
+ url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/timers.md",
47
+ },
48
+ type: "suggestion",
49
+ fixable: null,
50
+ schema: [{ enum: ["always", "never"] }],
51
+ messages: {
52
+ preferGlobal:
53
+ "Unexpected use of 'require(\"timers\").*'. Use the global variable instead.",
54
+ preferModule:
55
+ "Unexpected use of the global variable. Use 'require(\"timers\").*' instead.",
56
+ },
57
+ },
58
+
59
+ create(context) {
60
+ return {
61
+ "Program:exit"() {
62
+ checkForPreferGlobal(context, traceMap)
63
+ },
64
+ }
65
+ },
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-n",
3
- "version": "17.23.1",
3
+ "version": "17.24.0",
4
4
  "description": "Additional ESLint's rules for Node.js",
5
5
  "engines": {
6
6
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"