eslint-plugin-n 16.6.2 → 17.0.0-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
@@ -18,8 +18,11 @@ Additional ESLint rules for Node.js
18
18
  npm install --save-dev eslint eslint-plugin-n
19
19
  ```
20
20
 
21
- - Requires Node.js `>=16.0.0`
22
- - Requires ESLint `>=7.0.0`
21
+ | Version | Supported Node.js | Supported ESLint Version |
22
+ |---------|-------------------|---------------------------|
23
+ | 17.x | `^18.18.0 \|\| ^20.9.0 \|\| >=21.1.0` | `>=8.23.0` |
24
+ | 16.x | `>=16.0.0` | `>=7.0.0` |
25
+ | 15.x | `>=12.22.0` | `>=7.0.0` |
23
26
 
24
27
  **Note:** It recommends a use of [the "engines" field of package.json](https://docs.npmjs.com/files/package.json#engines). The "engines" field is used by `n/no-unsupported-features/*` rules.
25
28
 
@@ -15,14 +15,14 @@ const visitImport = require("../util/visit-import")
15
15
  * @returns {string[]} File extensions.
16
16
  */
17
17
  function getExistingExtensions(filePath) {
18
- const basename = path.basename(filePath, path.extname(filePath))
18
+ const directory = path.dirname(filePath)
19
+ const extension = path.extname(filePath)
20
+ const basename = path.basename(filePath, extension)
21
+
19
22
  try {
20
23
  return fs
21
- .readdirSync(path.dirname(filePath))
22
- .filter(
23
- filename =>
24
- path.basename(filename, path.extname(filename)) === basename
25
- )
24
+ .readdirSync(directory)
25
+ .filter(filename => filename.startsWith(`${basename}.`))
26
26
  .map(filename => path.extname(filename))
27
27
  } catch (_error) {
28
28
  return []
@@ -74,47 +74,56 @@ module.exports = {
74
74
  }
75
75
 
76
76
  // Get extension.
77
- const originalExt = path.extname(name)
78
- const existingExts = getExistingExtensions(filePath)
79
- const ext = path.extname(filePath) || existingExts.join(" or ")
80
- const style = overrideStyle[ext] || defaultStyle
77
+ const currentExt = path.extname(name)
78
+ const actualExt = path.extname(filePath)
79
+ const style = overrideStyle[actualExt] || defaultStyle
80
+
81
+ const expectedExt = mapTypescriptExtension(
82
+ context,
83
+ filePath,
84
+ actualExt
85
+ )
81
86
 
82
87
  // Verify.
83
- if (style === "always" && ext !== originalExt) {
84
- const fileExtensionToAdd = mapTypescriptExtension(
85
- context,
86
- filePath,
87
- ext
88
- )
88
+ if (style === "always" && currentExt !== expectedExt) {
89
89
  context.report({
90
90
  node,
91
91
  messageId: "requireExt",
92
- data: { ext: fileExtensionToAdd },
92
+ data: { ext: expectedExt },
93
93
  fix(fixer) {
94
- if (existingExts.length !== 1) {
95
- return null
96
- }
97
94
  const index = node.range[1] - 1
98
95
  return fixer.insertTextBeforeRange(
99
96
  [index, index],
100
- fileExtensionToAdd
97
+ expectedExt
101
98
  )
102
99
  },
103
100
  })
104
- } else if (style === "never" && ext === originalExt) {
101
+ }
102
+
103
+ if (
104
+ style === "never" &&
105
+ currentExt !== "" &&
106
+ expectedExt !== "" &&
107
+ currentExt === expectedExt
108
+ ) {
109
+ const otherExtensions = getExistingExtensions(filePath)
110
+
111
+ let fix = fixer => {
112
+ const index = name.lastIndexOf(currentExt)
113
+ const start = node.range[0] + 1 + index
114
+ const end = start + currentExt.length
115
+ return fixer.removeRange([start, end])
116
+ }
117
+
118
+ if (otherExtensions.length > 1) {
119
+ fix = undefined
120
+ }
121
+
105
122
  context.report({
106
123
  node,
107
124
  messageId: "forbidExt",
108
- data: { ext },
109
- fix(fixer) {
110
- if (existingExts.length !== 1) {
111
- return null
112
- }
113
- const index = name.lastIndexOf(ext)
114
- const start = node.range[0] + 1 + index
115
- const end = start + ext.length
116
- return fixer.removeRange([start, end])
117
- },
125
+ data: { ext: currentExt },
126
+ fix,
118
127
  })
119
128
  }
120
129
  }
@@ -9,11 +9,6 @@
9
9
  "use strict"
10
10
 
11
11
  const path = require("path")
12
- const resolve = require("resolve")
13
- const { pathToFileURL, fileURLToPath } = require("url")
14
- const {
15
- defaultResolve: importResolve,
16
- } = require("../converted-esm/import-meta-resolve")
17
12
  const getPackageJson = require("../util/get-package-json")
18
13
  const mergeVisitorsInPlace = require("../util/merge-visitors-in-place")
19
14
  const visitImport = require("../util/visit-import")
@@ -29,7 +24,8 @@ const CORE_MODULES = new Set([
29
24
  "crypto",
30
25
  "dgram",
31
26
  "dns",
32
- /* "domain", */ "events",
27
+ /* "domain", */
28
+ "events",
33
29
  "fs",
34
30
  "http",
35
31
  "https",
@@ -37,7 +33,8 @@ const CORE_MODULES = new Set([
37
33
  "net",
38
34
  "os",
39
35
  "path",
40
- /* "punycode", */ "querystring",
36
+ /* "punycode", */
37
+ "querystring",
41
38
  "readline",
42
39
  "repl",
43
40
  "stream",
@@ -132,22 +129,8 @@ module.exports = {
132
129
  continue
133
130
  }
134
131
 
135
- let resolved = ""
136
- const moduleId = `${name}/`
137
- try {
138
- resolved = resolve.sync(moduleId, {
139
- basedir: dirPath,
140
- })
141
- } catch (_error) {
142
- try {
143
- const { url } = importResolve(moduleId, {
144
- parentURL: pathToFileURL(dirPath).href,
145
- })
146
-
147
- resolved = fileURLToPath(url)
148
- } catch (_error) {
149
- continue
150
- }
132
+ if (target.filePath == null) {
133
+ continue
151
134
  }
152
135
 
153
136
  context.report({
@@ -156,7 +139,7 @@ module.exports = {
156
139
  messageId: "unexpectedImport",
157
140
  data: {
158
141
  name: path
159
- .relative(dirPath, resolved)
142
+ .relative(dirPath, target.filePath)
160
143
  .replace(BACK_SLASH, "/"),
161
144
  },
162
145
  })
@@ -10,6 +10,21 @@ const getAllowModules = require("./get-allow-modules")
10
10
  const isTypescript = require("./is-typescript")
11
11
  const mapTypescriptExtension = require("../util/map-typescript-extension")
12
12
 
13
+ /**
14
+ * Reports a missing file from ImportTarget
15
+ * @param {RuleContext} context - A context to report.
16
+ * @param {import('../util/import-target.js')} target - A list of target information to check.
17
+ * @returns {void}
18
+ */
19
+ function markMissing(context, target) {
20
+ context.report({
21
+ node: target.node,
22
+ loc: target.node.loc,
23
+ messageId: "notFound",
24
+ data: target,
25
+ })
26
+ }
27
+
13
28
  /**
14
29
  * Checks whether or not each requirement target exists.
15
30
  *
@@ -17,21 +32,33 @@ const mapTypescriptExtension = require("../util/map-typescript-extension")
17
32
  * See Also: https://nodejs.org/api/modules.html
18
33
  *
19
34
  * @param {RuleContext} context - A context to report.
20
- * @param {ImportTarget[]} targets - A list of target information to check.
35
+ * @param {import('../util/import-target.js')[]} targets - A list of target information to check.
21
36
  * @returns {void}
22
37
  */
23
38
  exports.checkExistence = function checkExistence(context, targets) {
24
39
  const allowed = new Set(getAllowModules(context))
25
40
 
26
41
  for (const target of targets) {
27
- const missingModule =
42
+ if (
28
43
  target.moduleName != null &&
29
44
  !allowed.has(target.moduleName) &&
30
45
  target.filePath == null
46
+ ) {
47
+ markMissing(context, target)
48
+ continue
49
+ }
50
+
51
+ if (target.moduleName != null) {
52
+ continue
53
+ }
54
+
55
+ let missingFile =
56
+ target.filePath == null ? false : !exists(target.filePath)
31
57
 
32
- let missingFile = target.moduleName == null && !exists(target.filePath)
33
58
  if (missingFile && isTypescript(context)) {
34
59
  const parsed = path.parse(target.filePath)
60
+ const pathWithoutExt = path.resolve(parsed.dir, parsed.name)
61
+
35
62
  const reversedExts = mapTypescriptExtension(
36
63
  context,
37
64
  target.filePath,
@@ -39,21 +66,16 @@ exports.checkExistence = function checkExistence(context, targets) {
39
66
  true
40
67
  )
41
68
  const reversedPaths = reversedExts.map(
42
- reversedExt =>
43
- path.resolve(parsed.dir, parsed.name) + reversedExt
69
+ reversedExt => pathWithoutExt + reversedExt
44
70
  )
45
71
  missingFile = reversedPaths.every(
46
72
  reversedPath =>
47
73
  target.moduleName == null && !exists(reversedPath)
48
74
  )
49
75
  }
50
- if (missingModule || missingFile) {
51
- context.report({
52
- node: target.node,
53
- loc: target.node.loc,
54
- messageId: "notFound",
55
- data: target,
56
- })
76
+
77
+ if (missingFile) {
78
+ markMissing(context, target)
57
79
  }
58
80
  }
59
81
  }
@@ -59,7 +59,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
59
59
  if (target.moduleName != null) {
60
60
  return false
61
61
  }
62
- const relativeTargetPath = toRelative(target.filePath)
62
+ const relativeTargetPath = toRelative(target.filePath ?? "")
63
63
  return (
64
64
  relativeTargetPath !== "" &&
65
65
  npmignore.match(relativeTargetPath)
@@ -70,6 +70,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
70
70
  devDependencies.has(target.moduleName) &&
71
71
  !dependencies.has(target.moduleName) &&
72
72
  !allowed.has(target.moduleName)
73
+
73
74
  if (isPrivateFile() || isDevPackage()) {
74
75
  context.report({
75
76
  node: target.node,
@@ -38,6 +38,10 @@ function existsCaseSensitive(filePath) {
38
38
  * @returns {boolean} `true` if the file of a given path exists.
39
39
  */
40
40
  module.exports = function exists(filePath) {
41
+ if (filePath == null) {
42
+ return false
43
+ }
44
+
41
45
  let result = cache.get(filePath)
42
46
  if (result == null) {
43
47
  try {
@@ -4,7 +4,26 @@
4
4
  */
5
5
  "use strict"
6
6
 
7
- const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
7
+ const { getTSConfigForContext } = require("./get-tsconfig")
8
+ const isTypescript = require("./is-typescript")
9
+
10
+ const DEFAULT_JS_VALUE = Object.freeze([
11
+ ".js",
12
+ ".json",
13
+ ".node",
14
+ ".mjs",
15
+ ".cjs",
16
+ ])
17
+ const DEFAULT_TS_VALUE = Object.freeze([
18
+ ".js",
19
+ ".ts",
20
+ ".mjs",
21
+ ".mts",
22
+ ".cjs",
23
+ ".cts",
24
+ ".json",
25
+ ".node",
26
+ ])
8
27
 
9
28
  /**
10
29
  * Gets `tryExtensions` property from a given option object.
@@ -13,7 +32,7 @@ const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
13
32
  * @returns {string[]|null} The `tryExtensions` value, or `null`.
14
33
  */
15
34
  function get(option) {
16
- if (option && option.tryExtensions && Array.isArray(option.tryExtensions)) {
35
+ if (Array.isArray(option?.tryExtensions)) {
17
36
  return option.tryExtensions.map(String)
18
37
  }
19
38
  return null
@@ -24,19 +43,29 @@ function get(option) {
24
43
  *
25
44
  * 1. This checks `options` property, then returns it if exists.
26
45
  * 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
27
- * 3. This returns `[".js", ".json", ".node"]`.
46
+ * 3. This returns `[".js", ".json", ".node", ".mjs", ".cjs"]`.
28
47
  *
29
48
  * @param {RuleContext} context - The rule context.
30
49
  * @returns {string[]} A list of extensions.
31
50
  */
32
51
  module.exports = function getTryExtensions(context, optionIndex = 0) {
33
- return (
34
- get(context.options && context.options[optionIndex]) ||
35
- get(
36
- context.settings && (context.settings.n || context.settings.node)
37
- ) ||
38
- DEFAULT_VALUE
39
- )
52
+ const configured =
53
+ get(context.options?.[optionIndex]) ??
54
+ get(context.settings?.n) ??
55
+ get(context.settings?.node)
56
+
57
+ if (configured != null) {
58
+ return configured
59
+ }
60
+
61
+ if (isTypescript(context)) {
62
+ const tsconfig = getTSConfigForContext(context)
63
+ if (tsconfig?.config?.compilerOptions?.allowImportingTsExtensions) {
64
+ return DEFAULT_TS_VALUE
65
+ }
66
+ }
67
+
68
+ return DEFAULT_JS_VALUE
40
69
  }
41
70
 
42
71
  module.exports.schema = {
@@ -17,15 +17,33 @@ function getTSConfig(filename) {
17
17
  * Attempts to get the ExtensionMap from the tsconfig of a given file.
18
18
  *
19
19
  * @param {string} filename - The path to the file we need to find the tsconfig.json of
20
- * @returns {import("get-tsconfig").TsConfigResult}
20
+ * @returns {import("get-tsconfig").TsConfigResult | null}
21
21
  */
22
22
  function getTSConfigForFile(filename) {
23
23
  return getTsconfig(filename, "tsconfig.json", fsCache)
24
24
  }
25
25
 
26
+ /**
27
+ * Attempts to get the ExtensionMap from the tsconfig of a given file.
28
+ *
29
+ * @param {import('eslint').Rule.RuleContext} context - The current eslint context
30
+ * @returns {import("get-tsconfig").TsConfigResult | null}
31
+ */
32
+ function getTSConfigForContext(context) {
33
+ // TODO: remove context.get(PhysicalFilename|Filename) when dropping eslint < v10
34
+ const filename =
35
+ context.physicalFilename ??
36
+ context.getPhysicalFilename?.() ??
37
+ context.filename ??
38
+ context.getFilename?.()
39
+
40
+ return getTSConfigForFile(filename)
41
+ }
42
+
26
43
  module.exports = {
27
44
  getTSConfig,
28
45
  getTSConfigForFile,
46
+ getTSConfigForContext,
29
47
  }
30
48
 
31
49
  module.exports.schema = { type: "string" }
@@ -1,6 +1,6 @@
1
1
  "use strict"
2
2
 
3
- const { getTSConfig, getTSConfigForFile } = require("./get-tsconfig")
3
+ const { getTSConfig, getTSConfigForContext } = require("./get-tsconfig")
4
4
 
5
5
  const DEFAULT_MAPPING = normalise([
6
6
  ["", ".js"],
@@ -32,9 +32,14 @@ const tsConfigMapping = {
32
32
  * @property {Record<string, string[]>} backward Convert from javascript to typescript
33
33
  */
34
34
 
35
+ /**
36
+ * @param {Record<string, string>} typescriptExtensionMap A forward extension mapping
37
+ * @returns {ExtensionMap}
38
+ */
35
39
  function normalise(typescriptExtensionMap) {
36
40
  const forward = {}
37
41
  const backward = {}
42
+
38
43
  for (const [typescript, javascript] of typescriptExtensionMap) {
39
44
  forward[typescript] = javascript
40
45
  if (!typescript) {
@@ -43,6 +48,7 @@ function normalise(typescriptExtensionMap) {
43
48
  backward[javascript] ??= []
44
49
  backward[javascript].push(typescript)
45
50
  }
51
+
46
52
  return { forward, backward }
47
53
  }
48
54
 
@@ -89,11 +95,11 @@ function get(option) {
89
95
  /**
90
96
  * Attempts to get the ExtensionMap from the tsconfig of a given file.
91
97
  *
92
- * @param {string} filename - The filename we're getting from
98
+ * @param {import('eslint').Rule.RuleContext} context - The current file context
93
99
  * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
94
100
  */
95
- function getFromTSConfigFromFile(filename) {
96
- return getMappingFromTSConfig(getTSConfigForFile(filename)?.config)
101
+ function getFromTSConfigFromFile(context) {
102
+ return getMappingFromTSConfig(getTSConfigForContext(context)?.config)
97
103
  }
98
104
 
99
105
  /**
@@ -109,18 +115,13 @@ function getFromTSConfigFromFile(filename) {
109
115
  * 8. This returns `PRESERVE_MAPPING`.
110
116
  *
111
117
  * @param {import("eslint").Rule.RuleContext} context - The rule context.
112
- * @returns {string[]} A list of extensions.
118
+ * @returns {ExtensionMap} A list of extensions.
113
119
  */
114
120
  module.exports = function getTypescriptExtensionMap(context) {
115
- const filename =
116
- context.physicalFilename ??
117
- context.getPhysicalFilename?.() ??
118
- context.filename ??
119
- context.getFilename?.() // TODO: remove context.get(PhysicalFilename|Filename) when dropping eslint < v10
120
121
  return (
121
122
  get(context.options?.[0]) ||
122
123
  get(context.settings?.n ?? context.settings?.node) ||
123
- getFromTSConfigFromFile(filename) ||
124
+ getFromTSConfigFromFile(context) ||
124
125
  PRESERVE_MAPPING
125
126
  )
126
127
  }