Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Start using enhanced-resolve to improve ts support #139

Merged
merged 15 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1,274 changes: 0 additions & 1,274 deletions lib/converted-esm/import-meta-resolve.js

This file was deleted.

73 changes: 41 additions & 32 deletions lib/rules/file-extension-in-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const visitImport = require("../util/visit-import")
* @returns {string[]} File extensions.
*/
function getExistingExtensions(filePath) {
const basename = path.basename(filePath, path.extname(filePath))
const directory = path.dirname(filePath)
const extension = path.extname(filePath)
const basename = path.basename(filePath, extension)

try {
return fs
.readdirSync(path.dirname(filePath))
.filter(
filename =>
path.basename(filename, path.extname(filename)) === basename
)
.readdirSync(directory)
.filter(filename => filename.startsWith(`${basename}.`))
.map(filename => path.extname(filename))
} catch (_error) {
return []
Expand Down Expand Up @@ -74,47 +74,56 @@ module.exports = {
}

// Get extension.
const originalExt = path.extname(name)
const existingExts = getExistingExtensions(filePath)
const ext = path.extname(filePath) || existingExts.join(" or ")
const style = overrideStyle[ext] || defaultStyle
const currentExt = path.extname(name)
const actualExt = path.extname(filePath)
const style = overrideStyle[actualExt] || defaultStyle

const expectedExt = mapTypescriptExtension(
context,
filePath,
actualExt
)

// Verify.
if (style === "always" && ext !== originalExt) {
const fileExtensionToAdd = mapTypescriptExtension(
context,
filePath,
ext
)
if (style === "always" && currentExt !== expectedExt) {
context.report({
node,
messageId: "requireExt",
data: { ext: fileExtensionToAdd },
data: { ext: expectedExt },
fix(fixer) {
if (existingExts.length !== 1) {
return null
}
const index = node.range[1] - 1
return fixer.insertTextBeforeRange(
[index, index],
fileExtensionToAdd
expectedExt
)
},
})
} else if (style === "never" && ext === originalExt) {
}

if (
style === "never" &&
currentExt !== "" &&
expectedExt !== "" &&
currentExt === expectedExt
) {
aladdin-add marked this conversation as resolved.
Show resolved Hide resolved
const otherExtensions = getExistingExtensions(filePath)

let fix = fixer => {
const index = name.lastIndexOf(currentExt)
const start = node.range[0] + 1 + index
const end = start + currentExt.length
return fixer.removeRange([start, end])
}

if (otherExtensions.length > 1) {
fix = undefined
}

context.report({
node,
messageId: "forbidExt",
data: { ext },
fix(fixer) {
if (existingExts.length !== 1) {
return null
}
const index = name.lastIndexOf(ext)
const start = node.range[0] + 1 + index
const end = start + ext.length
return fixer.removeRange([start, end])
},
data: { ext: currentExt },
fix,
})
}
}
Expand Down
31 changes: 7 additions & 24 deletions lib/rules/no-hide-core-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
"use strict"

const path = require("path")
const resolve = require("resolve")
const { pathToFileURL, fileURLToPath } = require("url")
const {
defaultResolve: importResolve,
} = require("../converted-esm/import-meta-resolve")
const getPackageJson = require("../util/get-package-json")
const mergeVisitorsInPlace = require("../util/merge-visitors-in-place")
const visitImport = require("../util/visit-import")
Expand All @@ -29,15 +24,17 @@ const CORE_MODULES = new Set([
"crypto",
"dgram",
"dns",
/* "domain", */ "events",
/* "domain", */
"events",
"fs",
"http",
"https",
"module",
"net",
"os",
"path",
/* "punycode", */ "querystring",
/* "punycode", */
"querystring",
"readline",
"repl",
"stream",
Expand Down Expand Up @@ -132,22 +129,8 @@ module.exports = {
continue
}

let resolved = ""
const moduleId = `${name}/`
try {
resolved = resolve.sync(moduleId, {
basedir: dirPath,
})
} catch (_error) {
try {
const { url } = importResolve(moduleId, {
parentURL: pathToFileURL(dirPath).href,
})

resolved = fileURLToPath(url)
} catch (_error) {
continue
}
if (target.filePath == null) {
continue
}

context.report({
Expand All @@ -156,7 +139,7 @@ module.exports = {
messageId: "unexpectedImport",
data: {
name: path
.relative(dirPath, resolved)
.relative(dirPath, target.filePath)
.replace(BACK_SLASH, "/"),
},
})
Expand Down
46 changes: 34 additions & 12 deletions lib/util/check-existence.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,72 @@ const getAllowModules = require("./get-allow-modules")
const isTypescript = require("./is-typescript")
const mapTypescriptExtension = require("../util/map-typescript-extension")

/**
* Reports a missing file from ImportTarget
* @param {RuleContext} context - A context to report.
* @param {import('../util/import-target.js')} target - A list of target information to check.
* @returns {void}
*/
function markMissing(context, target) {
context.report({
node: target.node,
loc: target.node.loc,
messageId: "notFound",
data: target,
})
}

/**
* Checks whether or not each requirement target exists.
*
* It looks up the target according to the logic of Node.js.
* See Also: https://nodejs.org/api/modules.html
*
* @param {RuleContext} context - A context to report.
* @param {ImportTarget[]} targets - A list of target information to check.
* @param {import('../util/import-target.js')[]} targets - A list of target information to check.
* @returns {void}
*/
exports.checkExistence = function checkExistence(context, targets) {
const allowed = new Set(getAllowModules(context))

for (const target of targets) {
const missingModule =
if (
target.moduleName != null &&
!allowed.has(target.moduleName) &&
target.filePath == null
) {
markMissing(context, target)
continue
}

if (target.moduleName != null) {
continue
}

let missingFile =
target.filePath == null ? false : !exists(target.filePath)

let missingFile = target.moduleName == null && !exists(target.filePath)
if (missingFile && isTypescript(context)) {
const parsed = path.parse(target.filePath)
const pathWithoutExt = path.resolve(parsed.dir, parsed.name)

const reversedExts = mapTypescriptExtension(
context,
target.filePath,
parsed.ext,
true
)
const reversedPaths = reversedExts.map(
reversedExt =>
path.resolve(parsed.dir, parsed.name) + reversedExt
reversedExt => pathWithoutExt + reversedExt
)
missingFile = reversedPaths.every(
reversedPath =>
target.moduleName == null && !exists(reversedPath)
)
}
if (missingModule || missingFile) {
context.report({
node: target.node,
loc: target.node.loc,
messageId: "notFound",
data: target,
})

if (missingFile) {
markMissing(context, target)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/util/check-publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
if (target.moduleName != null) {
return false
}
const relativeTargetPath = toRelative(target.filePath)
const relativeTargetPath = toRelative(target.filePath ?? "")
return (
relativeTargetPath !== "" &&
npmignore.match(relativeTargetPath)
Expand All @@ -70,6 +70,7 @@ exports.checkPublish = function checkPublish(context, filePath, targets) {
devDependencies.has(target.moduleName) &&
!dependencies.has(target.moduleName) &&
!allowed.has(target.moduleName)

if (isPrivateFile() || isDevPackage()) {
context.report({
node: target.node,
Expand Down
4 changes: 4 additions & 0 deletions lib/util/exists.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ function existsCaseSensitive(filePath) {
* @returns {boolean} `true` if the file of a given path exists.
*/
module.exports = function exists(filePath) {
if (filePath == null) {
return false
}

let result = cache.get(filePath)
if (result == null) {
try {
Expand Down
49 changes: 39 additions & 10 deletions lib/util/get-try-extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,26 @@
*/
"use strict"

const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
const { getTSConfigForContext } = require("./get-tsconfig")
const isTypescript = require("./is-typescript")

const DEFAULT_JS_VALUE = Object.freeze([
".js",
".json",
".node",
".mjs",
".cjs",
])
const DEFAULT_TS_VALUE = Object.freeze([
".js",
".ts",
".mjs",
".mts",
".cjs",
".cts",
".json",
".node",
])

/**
* Gets `tryExtensions` property from a given option object.
Expand All @@ -13,7 +32,7 @@ const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
* @returns {string[]|null} The `tryExtensions` value, or `null`.
*/
function get(option) {
if (option && option.tryExtensions && Array.isArray(option.tryExtensions)) {
if (Array.isArray(option?.tryExtensions)) {
return option.tryExtensions.map(String)
}
return null
Expand All @@ -24,19 +43,29 @@ function get(option) {
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns `[".js", ".json", ".node"]`.
* 3. This returns `[".js", ".json", ".node", ".mjs", ".cjs"]`.
*
* @param {RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getTryExtensions(context, optionIndex = 0) {
return (
get(context.options && context.options[optionIndex]) ||
get(
context.settings && (context.settings.n || context.settings.node)
) ||
DEFAULT_VALUE
)
const configured =
get(context.options?.[optionIndex]) ??
get(context.settings?.n) ??
get(context.settings?.node)

if (configured != null) {
return configured
}

if (isTypescript(context)) {
const tsconfig = getTSConfigForContext(context)
if (tsconfig?.config?.compilerOptions?.allowImportingTsExtensions) {
return DEFAULT_TS_VALUE
}
}

return DEFAULT_JS_VALUE
}

module.exports.schema = {
Expand Down
20 changes: 19 additions & 1 deletion lib/util/get-tsconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,33 @@ function getTSConfig(filename) {
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {string} filename - The path to the file we need to find the tsconfig.json of
* @returns {import("get-tsconfig").TsConfigResult}
* @returns {import("get-tsconfig").TsConfigResult | null}
*/
function getTSConfigForFile(filename) {
return getTsconfig(filename, "tsconfig.json", fsCache)
}

/**
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {import('eslint').Rule.RuleContext} context - The current eslint context
* @returns {import("get-tsconfig").TsConfigResult | null}
*/
function getTSConfigForContext(context) {
// TODO: remove context.get(PhysicalFilename|Filename) when dropping eslint < v10
const filename =
context.physicalFilename ??
context.getPhysicalFilename?.() ??
context.filename ??
context.getFilename?.()

return getTSConfigForFile(filename)
}

module.exports = {
getTSConfig,
getTSConfigForFile,
getTSConfigForContext,
}

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