Skip to content

Commit dc9f473

Browse files
scagoodaladdin-add
andauthoredJan 9, 2024
feat!: Start using enhanced-resolve to improve ts support (#139)
* feat: Use enhanced-resolve for imports * chore: Improve the metadata from "ImportTarget" * chore: remove "enhanced-resolve" from "no-hide-core-modules" * test: Add a test for #66 * feat!: Allow ts paths aliases (#84) * feat: Allow for "allowImportingTsExtensions" (#134) * feat: Add test for import maps (#147) * Update lib/util/import-target.js Co-authored-by: Sebastian Good <2230835+scagood@users.noreply.github.com> * test: Add test for n/no-missing-require eslint/use-at-your-own-risk * chore: Remove esbuild * feat: Allow for settings.cwd to be used before process.cwd * chore: replace reference to eslint/use-at-your-own-risk * chore: Remove more unused packages * chore: update rule test options to flat config * fix: incorrect env in tests --------- Co-authored-by: 唯然 <weiran.zsd@outlook.com>
1 parent 5449752 commit dc9f473

31 files changed

+492
-1499
lines changed
 

‎lib/converted-esm/import-meta-resolve.js

-1,274
This file was deleted.

‎lib/rules/file-extension-in-import.js

+41-32
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ const visitImport = require("../util/visit-import")
1515
* @returns {string[]} File extensions.
1616
*/
1717
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+
1922
try {
2023
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}.`))
2626
.map(filename => path.extname(filename))
2727
} catch (_error) {
2828
return []
@@ -74,47 +74,56 @@ module.exports = {
7474
}
7575

7676
// 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+
)
8186

8287
// Verify.
83-
if (style === "always" && ext !== originalExt) {
84-
const fileExtensionToAdd = mapTypescriptExtension(
85-
context,
86-
filePath,
87-
ext
88-
)
88+
if (style === "always" && currentExt !== expectedExt) {
8989
context.report({
9090
node,
9191
messageId: "requireExt",
92-
data: { ext: fileExtensionToAdd },
92+
data: { ext: expectedExt },
9393
fix(fixer) {
94-
if (existingExts.length !== 1) {
95-
return null
96-
}
9794
const index = node.range[1] - 1
9895
return fixer.insertTextBeforeRange(
9996
[index, index],
100-
fileExtensionToAdd
97+
expectedExt
10198
)
10299
},
103100
})
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+
105122
context.report({
106123
node,
107124
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,
118127
})
119128
}
120129
}

‎lib/rules/no-hide-core-modules.js

+7-24
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99
"use strict"
1010

1111
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")
1712
const getPackageJson = require("../util/get-package-json")
1813
const mergeVisitorsInPlace = require("../util/merge-visitors-in-place")
1914
const visitImport = require("../util/visit-import")
@@ -29,15 +24,17 @@ const CORE_MODULES = new Set([
2924
"crypto",
3025
"dgram",
3126
"dns",
32-
/* "domain", */ "events",
27+
/* "domain", */
28+
"events",
3329
"fs",
3430
"http",
3531
"https",
3632
"module",
3733
"net",
3834
"os",
3935
"path",
40-
/* "punycode", */ "querystring",
36+
/* "punycode", */
37+
"querystring",
4138
"readline",
4239
"repl",
4340
"stream",
@@ -132,22 +129,8 @@ module.exports = {
132129
continue
133130
}
134131

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
151134
}
152135

153136
context.report({
@@ -156,7 +139,7 @@ module.exports = {
156139
messageId: "unexpectedImport",
157140
data: {
158141
name: path
159-
.relative(dirPath, resolved)
142+
.relative(dirPath, target.filePath)
160143
.replace(BACK_SLASH, "/"),
161144
},
162145
})

‎lib/util/check-existence.js

+34-12
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,72 @@ const getAllowModules = require("./get-allow-modules")
1010
const isTypescript = require("./is-typescript")
1111
const mapTypescriptExtension = require("../util/map-typescript-extension")
1212

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+
1328
/**
1429
* Checks whether or not each requirement target exists.
1530
*
1631
* It looks up the target according to the logic of Node.js.
1732
* See Also: https://nodejs.org/api/modules.html
1833
*
1934
* @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.
2136
* @returns {void}
2237
*/
2338
exports.checkExistence = function checkExistence(context, targets) {
2439
const allowed = new Set(getAllowModules(context))
2540

2641
for (const target of targets) {
27-
const missingModule =
42+
if (
2843
target.moduleName != null &&
2944
!allowed.has(target.moduleName) &&
3045
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)
3157

32-
let missingFile = target.moduleName == null && !exists(target.filePath)
3358
if (missingFile && isTypescript(context)) {
3459
const parsed = path.parse(target.filePath)
60+
const pathWithoutExt = path.resolve(parsed.dir, parsed.name)
61+
3562
const reversedExts = mapTypescriptExtension(
3663
context,
3764
target.filePath,
3865
parsed.ext,
3966
true
4067
)
4168
const reversedPaths = reversedExts.map(
42-
reversedExt =>
43-
path.resolve(parsed.dir, parsed.name) + reversedExt
69+
reversedExt => pathWithoutExt + reversedExt
4470
)
4571
missingFile = reversedPaths.every(
4672
reversedPath =>
4773
target.moduleName == null && !exists(reversedPath)
4874
)
4975
}
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)
5779
}
5880
}
5981
}

‎lib/util/check-publish.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
5959
if (target.moduleName != null) {
6060
return false
6161
}
62-
const relativeTargetPath = toRelative(target.filePath)
62+
const relativeTargetPath = toRelative(target.filePath ?? "")
6363
return (
6464
relativeTargetPath !== "" &&
6565
npmignore.match(relativeTargetPath)
@@ -70,6 +70,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
7070
devDependencies.has(target.moduleName) &&
7171
!dependencies.has(target.moduleName) &&
7272
!allowed.has(target.moduleName)
73+
7374
if (isPrivateFile() || isDevPackage()) {
7475
context.report({
7576
node: target.node,

‎lib/util/exists.js

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ function existsCaseSensitive(filePath) {
3838
* @returns {boolean} `true` if the file of a given path exists.
3939
*/
4040
module.exports = function exists(filePath) {
41+
if (filePath == null) {
42+
return false
43+
}
44+
4145
let result = cache.get(filePath)
4246
if (result == null) {
4347
try {

‎lib/util/get-try-extensions.js

+39-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,26 @@
44
*/
55
"use strict"
66

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+
])
827

928
/**
1029
* Gets `tryExtensions` property from a given option object.
@@ -13,7 +32,7 @@ const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
1332
* @returns {string[]|null} The `tryExtensions` value, or `null`.
1433
*/
1534
function get(option) {
16-
if (option && option.tryExtensions && Array.isArray(option.tryExtensions)) {
35+
if (Array.isArray(option?.tryExtensions)) {
1736
return option.tryExtensions.map(String)
1837
}
1938
return null
@@ -24,19 +43,29 @@ function get(option) {
2443
*
2544
* 1. This checks `options` property, then returns it if exists.
2645
* 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"]`.
2847
*
2948
* @param {RuleContext} context - The rule context.
3049
* @returns {string[]} A list of extensions.
3150
*/
3251
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
4069
}
4170

4271
module.exports.schema = {

‎lib/util/get-tsconfig.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,33 @@ function getTSConfig(filename) {
1717
* Attempts to get the ExtensionMap from the tsconfig of a given file.
1818
*
1919
* @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}
2121
*/
2222
function getTSConfigForFile(filename) {
2323
return getTsconfig(filename, "tsconfig.json", fsCache)
2424
}
2525

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+
2643
module.exports = {
2744
getTSConfig,
2845
getTSConfigForFile,
46+
getTSConfigForContext,
2947
}
3048

3149
module.exports.schema = { type: "string" }

‎lib/util/get-typescript-extension-map.js

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use strict"
22

3-
const { getTSConfig, getTSConfigForFile } = require("./get-tsconfig")
3+
const { getTSConfig, getTSConfigForContext } = require("./get-tsconfig")
44

55
const DEFAULT_MAPPING = normalise([
66
["", ".js"],
@@ -32,9 +32,14 @@ const tsConfigMapping = {
3232
* @property {Record<string, string[]>} backward Convert from javascript to typescript
3333
*/
3434

35+
/**
36+
* @param {Record<string, string>} typescriptExtensionMap A forward extension mapping
37+
* @returns {ExtensionMap}
38+
*/
3539
function normalise(typescriptExtensionMap) {
3640
const forward = {}
3741
const backward = {}
42+
3843
for (const [typescript, javascript] of typescriptExtensionMap) {
3944
forward[typescript] = javascript
4045
if (!typescript) {
@@ -43,6 +48,7 @@ function normalise(typescriptExtensionMap) {
4348
backward[javascript] ??= []
4449
backward[javascript].push(typescript)
4550
}
51+
4652
return { forward, backward }
4753
}
4854

@@ -89,11 +95,11 @@ function get(option) {
8995
/**
9096
* Attempts to get the ExtensionMap from the tsconfig of a given file.
9197
*
92-
* @param {string} filename - The filename we're getting from
98+
* @param {import('eslint').Rule.RuleContext} context - The current file context
9399
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
94100
*/
95-
function getFromTSConfigFromFile(filename) {
96-
return getMappingFromTSConfig(getTSConfigForFile(filename)?.config)
101+
function getFromTSConfigFromFile(context) {
102+
return getMappingFromTSConfig(getTSConfigForContext(context)?.config)
97103
}
98104

99105
/**
@@ -109,18 +115,13 @@ function getFromTSConfigFromFile(filename) {
109115
* 8. This returns `PRESERVE_MAPPING`.
110116
*
111117
* @param {import("eslint").Rule.RuleContext} context - The rule context.
112-
* @returns {string[]} A list of extensions.
118+
* @returns {ExtensionMap} A list of extensions.
113119
*/
114120
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
120121
return (
121122
get(context.options?.[0]) ||
122123
get(context.settings?.n ?? context.settings?.node) ||
123-
getFromTSConfigFromFile(filename) ||
124+
getFromTSConfigFromFile(context) ||
124125
PRESERVE_MAPPING
125126
)
126127
}

‎lib/util/import-target.js

+232-106
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,57 @@
44
*/
55
"use strict"
66

7-
const path = require("path")
8-
const { pathToFileURL, fileURLToPath } = require("url")
7+
require("util").inspect.defaultOptions.depth = null
8+
9+
const { resolve } = require("path")
910
const isBuiltin = require("is-builtin-module")
10-
const resolve = require("resolve")
11-
const {
12-
defaultResolve: importResolve,
13-
} = require("../converted-esm/import-meta-resolve")
11+
const resolver = require("enhanced-resolve")
1412

15-
/**
16-
* Resolve the given id to file paths.
17-
* @param {boolean} isModule The flag which indicates this id is a module.
18-
* @param {string} id The id to resolve.
19-
* @param {object} options The options of node-resolve module.
20-
* It requires `options.basedir`.
21-
* @param {'import' | 'require'} moduleType - whether the target was require-ed or imported
22-
* @returns {string|null} The resolved path.
23-
*/
24-
function getFilePath(isModule, id, options, moduleType) {
25-
if (moduleType === "import") {
26-
const paths =
27-
options.paths && options.paths.length > 0
28-
? options.paths.map(p => path.resolve(process.cwd(), p))
29-
: [options.basedir]
30-
for (const aPath of paths) {
31-
try {
32-
const { url } = importResolve(id, {
33-
parentURL: pathToFileURL(path.join(aPath, "dummy-file.mjs"))
34-
.href,
35-
conditions: ["node", "import", "require"],
36-
})
37-
38-
if (url) {
39-
return fileURLToPath(url)
40-
}
41-
} catch (e) {
42-
continue
43-
}
44-
}
13+
const isTypescript = require("./is-typescript")
14+
const { getTSConfigForContext } = require("./get-tsconfig.js")
15+
const getTypescriptExtensionMap = require("./get-typescript-extension-map")
4516

46-
if (isModule) {
47-
return null
48-
}
49-
return path.resolve(
50-
(options.paths && options.paths[0]) || options.basedir,
51-
id
52-
)
53-
} else {
54-
try {
55-
return resolve.sync(id, options)
56-
} catch (_err) {
57-
try {
58-
const { url } = importResolve(id, {
59-
parentURL: pathToFileURL(
60-
path.join(options.basedir, "dummy-file.js")
61-
).href,
62-
conditions: ["node", "require"],
63-
})
64-
65-
return fileURLToPath(url)
66-
} catch (err) {
67-
if (isModule) {
68-
return null
69-
}
70-
return path.resolve(options.basedir, id)
71-
}
72-
}
17+
function removeTrailWildcard(input) {
18+
if (Array.isArray(input)) {
19+
return [...input].map(removeTrailWildcard)
7320
}
21+
22+
return input.replace(/[/\\*]+$/, "")
7423
}
7524

76-
function isNodeModule(name, options) {
77-
try {
78-
return require.resolve(name, options).startsWith(path.sep)
79-
} catch {
80-
return false
25+
/**
26+
* Initialize this instance.
27+
* @param {import('eslint').Rule.RuleContext} context - The context for the import origin.
28+
* @returns {import('enhanced-resolve').ResolveOptions['alias'] | undefined}
29+
*/
30+
function getTSConfigAliases(context) {
31+
const tsConfig = getTSConfigForContext(context)
32+
33+
const paths = tsConfig?.config?.compilerOptions?.paths
34+
35+
if (paths == null) {
36+
return
8137
}
38+
39+
return Object.entries(paths).map(([name, alias]) => ({
40+
name: removeTrailWildcard(name),
41+
alias: removeTrailWildcard(alias),
42+
}))
8243
}
8344

8445
/**
85-
* Gets the module name of a given path.
86-
*
87-
* e.g. `eslint/lib/ast-utils` -> `eslint`
88-
*
89-
* @param {string} nameOrPath - A path to get.
90-
* @returns {string} The module name of the path.
46+
* @typedef {Object} Options
47+
* @property {string[]} [extensions]
48+
* @property {string[]} [paths]
49+
* @property {string} basedir
50+
*/
51+
/**
52+
* @typedef { 'unknown' | 'relative' | 'absolute' | 'node' | 'npm' | 'http' } ModuleType
53+
* @typedef { 'import' | 'require' | 'type' } ModuleStyle
9154
*/
92-
function getModuleName(nameOrPath) {
93-
let end = nameOrPath.indexOf("/")
94-
if (end !== -1 && nameOrPath[0] === "@") {
95-
end = nameOrPath.indexOf("/", 1 + end)
96-
}
9755

98-
return end === -1 ? nameOrPath : nameOrPath.slice(0, end)
56+
function trimAfter(string, matcher, count = 1) {
57+
return string.split(matcher).slice(0, count).join(matcher)
9958
}
10059

10160
/**
@@ -104,17 +63,22 @@ function getModuleName(nameOrPath) {
10463
module.exports = class ImportTarget {
10564
/**
10665
* Initialize this instance.
107-
* @param {ASTNode} node - The node of a `require()` or a module declaraiton.
66+
* @param {import('eslint').Rule.RuleContext} context - The context for the import origin.
67+
* @param {import('eslint').Rule.Node} node - The node of a `require()` or a module declaraiton.
10868
* @param {string} name - The name of an import target.
109-
* @param {object} options - The options of `node-resolve` module.
69+
* @param {Options} options - The options of `enhanced-resolve` module.
11070
* @param {'import' | 'require'} moduleType - whether the target was require-ed or imported
11171
*/
112-
constructor(node, name, options, moduleType) {
113-
const isModule = !/^(?:[./\\]|\w+:)/u.test(name)
72+
constructor(context, node, name, options, moduleType) {
73+
/**
74+
* The context for the import origin
75+
* @type {import('eslint').Rule.Node}
76+
*/
77+
this.context = context
11478

11579
/**
11680
* The node of a `require()` or a module declaraiton.
117-
* @type {ASTNode}
81+
* @type {import('eslint').Rule.Node}
11882
*/
11983
this.node = node
12084

@@ -125,35 +89,197 @@ module.exports = class ImportTarget {
12589
this.name = name
12690

12791
/**
128-
* What type of module is this
129-
* @type {'unknown'|'relative'|'absolute'|'node'|'npm'|'http'|void}
92+
* The import target options.
93+
* @type {Options}
13094
*/
131-
this.moduleType = "unknown"
132-
133-
if (name.startsWith("./") || name.startsWith(".\\")) {
134-
this.moduleType = "relative"
135-
} else if (name.startsWith("/") || name.startsWith("\\")) {
136-
this.moduleType = "absolute"
137-
} else if (isBuiltin(name)) {
138-
this.moduleType = "node"
139-
} else if (isNodeModule(name, options)) {
140-
this.moduleType = "npm"
141-
} else if (name.startsWith("http://") || name.startsWith("https://")) {
142-
this.moduleType = "http"
143-
}
95+
this.options = options
14496

14597
/**
146-
* The full path of this import target.
147-
* If the target is a module and it does not exist then this is `null`.
148-
* @type {string|null}
98+
* What type of module are we looking for?
99+
* @type {ModuleType}
149100
*/
150-
this.filePath = getFilePath(isModule, name, options, moduleType)
101+
this.moduleType = this.getModuleType()
102+
103+
/**
104+
* What import style are we using
105+
* @type {ModuleStyle}
106+
*/
107+
this.moduleStyle = this.getModuleStyle(moduleType)
151108

152109
/**
153110
* The module name of this import target.
154111
* If the target is a relative path then this is `null`.
155-
* @type {string|null}
112+
* @type {string | null}
113+
*/
114+
this.moduleName = this.getModuleName()
115+
116+
/**
117+
* The full path of this import target.
118+
* If the target is a module and it does not exist then this is `null`.
119+
* @type {string | null}
156120
*/
157-
this.moduleName = isModule ? getModuleName(name) : null
121+
this.filePath = this.getFilePath()
122+
}
123+
124+
/**
125+
* What type of module is this
126+
* @returns {ModuleType}
127+
*/
128+
getModuleType() {
129+
if (/^\.{1,2}([\\/]|$)/.test(this.name)) {
130+
return "relative"
131+
}
132+
133+
if (/^[\\/]/.test(this.name)) {
134+
return "absolute"
135+
}
136+
137+
if (isBuiltin(this.name)) {
138+
return "node"
139+
}
140+
141+
if (/^(@[\w~-][\w.~-]*\/)?[\w~-][\w.~-]*/.test(this.name)) {
142+
return "npm"
143+
}
144+
145+
if (/^https?:\/\//.test(this.name)) {
146+
return "http"
147+
}
148+
149+
return "unknown"
150+
}
151+
152+
/**
153+
* What module import style is used
154+
* @param {'import' | 'require'} fallback
155+
* @returns {ModuleStyle}
156+
*/
157+
getModuleStyle(fallback) {
158+
/** @type {import('eslint').Rule.Node} */
159+
let node = { parent: this.node }
160+
161+
do {
162+
node = node.parent
163+
164+
// `const {} = require('')`
165+
if (
166+
node.type === "CallExpression" &&
167+
node.callee.name === "require"
168+
) {
169+
return "require"
170+
}
171+
172+
// `import type {} from '';`
173+
if (
174+
node.type === "ImportDeclaration" &&
175+
node.importKind === "type"
176+
) {
177+
return "type"
178+
}
179+
180+
// `import {} from '';`
181+
if (
182+
node.type === "ImportDeclaration" &&
183+
node.importKind === "value"
184+
) {
185+
return "import"
186+
}
187+
} while (node.parent)
188+
189+
return fallback
190+
}
191+
192+
/**
193+
* Get the node or npm module name
194+
* @returns {string}
195+
*/
196+
getModuleName() {
197+
if (this.moduleType === "relative") return
198+
199+
if (this.moduleType === "npm") {
200+
if (this.name.startsWith("@")) {
201+
return trimAfter(this.name, "/", 2)
202+
}
203+
204+
return trimAfter(this.name, "/")
205+
}
206+
207+
if (this.moduleType === "node") {
208+
if (this.name.startsWith("node:")) {
209+
return trimAfter(this.name.slice(5), "/")
210+
}
211+
212+
return trimAfter(this.name, "/")
213+
}
214+
}
215+
216+
getPaths() {
217+
if (Array.isArray(this.options.paths)) {
218+
return [...this.options.paths, this.options.basedir]
219+
}
220+
221+
return [this.options.basedir]
222+
}
223+
224+
/**
225+
* Resolve the given id to file paths.
226+
* @returns {string | null} The resolved path.
227+
*/
228+
getFilePath() {
229+
const conditionNames = ["node", "require"]
230+
const { extensions } = this.options
231+
const mainFields = []
232+
const mainFiles = []
233+
234+
if (this.moduleStyle === "import") {
235+
conditionNames.push("import")
236+
}
237+
238+
if (this.moduleStyle === "type") {
239+
conditionNames.push("import", "types")
240+
}
241+
242+
if (
243+
this.moduleStyle === "require" ||
244+
this.moduleType === "npm" ||
245+
this.moduleType === "node"
246+
) {
247+
mainFields.push("main")
248+
mainFiles.push("index")
249+
}
250+
251+
let alias = undefined
252+
let extensionAlias = undefined
253+
254+
if (isTypescript(this.context)) {
255+
alias = getTSConfigAliases(this.context)
256+
extensionAlias = getTypescriptExtensionMap(this.context).backward
257+
}
258+
259+
const requireResolve = resolver.create.sync({
260+
conditionNames,
261+
extensions,
262+
mainFields,
263+
mainFiles,
264+
265+
extensionAlias,
266+
alias,
267+
})
268+
269+
const cwd = this.context.settings?.cwd ?? process.cwd()
270+
for (const directory of this.getPaths()) {
271+
try {
272+
const baseDir = resolve(cwd, directory)
273+
return requireResolve(baseDir, this.name)
274+
} catch {
275+
continue
276+
}
277+
}
278+
279+
if (this.moduleType === "absolute" || this.moduleType === "relative") {
280+
return resolve(this.options.basedir, this.name)
281+
}
282+
283+
return null
158284
}
159285
}

‎lib/util/visit-import.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ module.exports = function visitImport(
6868
// Note: "999" arbitrary to check current/future Node.js version
6969
if (name && (includeCore || !isCoreModule(name, "999"))) {
7070
targets.push(
71-
new ImportTarget(sourceNode, name, options, "import")
71+
new ImportTarget(
72+
context,
73+
sourceNode,
74+
name,
75+
options,
76+
"import"
77+
)
7278
)
7379
}
7480
},

‎lib/util/visit-require.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ module.exports = function visitRequire(
6060
// Note: "999" arbitrary to check current/future Node.js version
6161
if (name && (includeCore || !isCoreModule(name, "999"))) {
6262
targets.push(
63-
new ImportTarget(targetNode, name, options, "require")
63+
new ImportTarget(
64+
context,
65+
targetNode,
66+
name,
67+
options,
68+
"require"
69+
)
6470
)
6571
}
6672
}

‎package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,27 @@
1515
},
1616
"dependencies": {
1717
"@eslint-community/eslint-utils": "^4.4.0",
18-
"builtins": "^5.0.1",
18+
"enhanced-resolve": "^5.15.0",
1919
"eslint-plugin-es-x": "^7.5.0",
2020
"get-tsconfig": "^4.7.0",
2121
"globals": "^13.24.0",
2222
"ignore": "^5.2.4",
2323
"is-builtin-module": "^3.2.1",
2424
"is-core-module": "^2.12.1",
2525
"minimatch": "^3.1.2",
26-
"resolve": "^1.22.2",
2726
"semver": "^7.5.3"
2827
},
2928
"devDependencies": {
3029
"@eslint/js": "^8.43.0",
3130
"@types/eslint": "^8.44.6",
3231
"@typescript-eslint/parser": "^5.60.0",
33-
"esbuild": "^0.18.7",
3432
"eslint": "^8",
3533
"eslint-config-prettier": "^8.8.0",
3634
"eslint-doc-generator": "^1.6.1",
3735
"eslint-plugin-eslint-plugin": "^5.2.1",
3836
"eslint-plugin-n": "file:.",
3937
"fast-glob": "^3.2.12",
4038
"husky": "^8.0.3",
41-
"import-meta-resolve": "^3.0.0",
4239
"lint-staged": "^13.2.2",
4340
"markdownlint-cli": "^0.35.0",
4441
"mocha": "^10.2.0",

‎scripts/convert-pure-esm-to-cjs.js

-6
This file was deleted.

‎tests/fixtures/file-extension-in-import/ts-allow-extension/file.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"noEmit": true,
4+
"allowImportingTsExtensions": true
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"imports": {
3+
"#b": "./src/a.js"
4+
}
5+
}

‎tests/fixtures/no-extraneous/import-map/src/b.js

Whitespace-only changes.

‎tests/fixtures/no-missing/node_modules/types-only/package.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/fixtures/no-missing/node_modules/types-only/types.d.ts

Whitespace-only changes.

‎tests/fixtures/no-missing/ts-allow-extension/file.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"noEmit": true,
4+
"allowImportingTsExtensions": true
5+
}
6+
}

‎tests/fixtures/no-missing/ts-paths/some/where.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"paths": {
4+
"@direct": ["./some/where.ts"],
5+
"@wild/*": ["./some/*"]
6+
}
7+
}
8+
}

‎tests/lib/rules/file-extension-in-import.js

+22-6
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ new RuleTester({
193193
code: "require('./e.js');",
194194
settings: { node: { typescriptExtensionMap: tsReactExtensionMap } },
195195
},
196+
197+
{
198+
filename: fixture("ts-allow-extension/test.ts"),
199+
code: "require('./file.js');",
200+
},
201+
{
202+
filename: fixture("ts-allow-extension/test.ts"),
203+
code: "require('./file.ts');",
204+
},
196205
],
197206
invalid: [
198207
{
@@ -309,21 +318,28 @@ new RuleTester({
309318
options: ["never", { ".json": "always" }],
310319
errors: [{ messageId: "forbidExt", data: { ext: ".mjs" } }],
311320
},
321+
312322
{
323+
// name: '.js has a higher priority than .json'
313324
filename: fixture("test.js"),
314325
code: "import './multi'",
315-
output: null,
326+
output: "import './multi.js'",
316327
options: ["always"],
317-
errors: [
318-
{ messageId: "requireExt", data: { ext: ".cjs or .mjs" } },
319-
],
328+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
320329
},
321330
{
322331
filename: fixture("test.js"),
323-
code: "import './multi.cjs'",
332+
code: "import './multi.js'",
324333
output: null,
325334
options: ["never"],
326-
errors: [{ messageId: "forbidExt", data: { ext: ".cjs" } }],
335+
errors: [{ messageId: "forbidExt", data: { ext: ".js" } }],
336+
},
337+
{
338+
filename: fixture("test.js"),
339+
code: "import './multi.json'",
340+
output: null,
341+
options: ["never"],
342+
errors: [{ messageId: "forbidExt", data: { ext: ".json" } }],
327343
},
328344

329345
// import()

‎tests/lib/rules/no-extraneous-import.js

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ ruleTester.run("no-extraneous-import", rule, {
6868
filename: fixture("optionalDependencies/a.js"),
6969
code: "import aaa from 'aaa'",
7070
},
71+
{
72+
filename: fixture("import-map/a.js"),
73+
code: "import '#b'",
74+
},
7175

7276
// missing packages are warned by no-missing-import
7377
{

‎tests/lib/rules/no-missing-import.js

+27
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,33 @@ ruleTester.run("no-missing-import", rule, {
266266
code: "import d from './d.js';",
267267
},
268268

269+
{
270+
// name: "tsconfig - compilerOptions.paths - direct reference",
271+
filename: fixture("ts-paths/test.ts"),
272+
code: "import before from '@direct';",
273+
},
274+
{
275+
// name: "tsconfig - compilerOptions.paths - wildcard reference",
276+
filename: fixture("ts-paths/test.ts"),
277+
code: "import before from '@wild/where.js';",
278+
},
279+
280+
{
281+
// name: 'Ensure type only packages can be imported',
282+
filename: fixture("test.ts"),
283+
languageOptions: { parser: require("@typescript-eslint/parser") },
284+
code: "import type d from 'types-only';",
285+
},
286+
287+
{
288+
filename: fixture("ts-allow-extension/test.ts"),
289+
code: "import './file.js';",
290+
},
291+
{
292+
filename: fixture("ts-allow-extension/test.ts"),
293+
code: "import './file.ts';",
294+
},
295+
269296
// import()
270297
...(DynamicImportSupported
271298
? [

‎tests/lib/rules/no-missing-require.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ruleTester.run("no-missing-require", rule, {
4242
},
4343
{
4444
filename: fixture("test.js"),
45-
code: "require('eslint/lib/api');",
45+
code: "require('rimraf/package.json');",
4646
},
4747
{
4848
filename: fixture("test.js"),
@@ -100,19 +100,19 @@ ruleTester.run("no-missing-require", rule, {
100100
// resolvePaths
101101
{
102102
filename: fixture("test.js"),
103-
code: "require('fixtures/no-missing/a');",
103+
code: "require('./fixtures/no-missing/a');",
104104
settings: {
105105
node: { resolvePaths: [path.resolve(__dirname, "../../")] },
106106
},
107107
},
108108
{
109109
filename: fixture("test.js"),
110-
code: "require('fixtures/no-missing/a');",
110+
code: "require('./fixtures/no-missing/a');",
111111
options: [{ resolvePaths: [path.resolve(__dirname, "../../")] }],
112112
},
113113
{
114114
filename: fixture("test.js"),
115-
code: "require('fixtures/no-missing/a');",
115+
code: "require('./fixtures/no-missing/a');",
116116
options: [{ resolvePaths: ["tests"] }],
117117
},
118118

‎tests/lib/rules/no-unpublished-import.js

-6
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,6 @@ ruleTester.run("no-unpublished-import", rule, {
129129
options: [{ allowModules: ["electron"] }],
130130
},
131131

132-
// Should not fill in the extension
133-
{
134-
filename: fixture("2/test.js"),
135-
code: "import ignore1 from './ignore1';",
136-
},
137-
138132
// Auto-published files only apply to root package directory
139133
{
140134
filename: fixture("3/src/readme.js"),

0 commit comments

Comments
 (0)
Please sign in to comment.