scripts/eslint-plugin-esm-imports.mjs
Rama main · réplica de lectura del repositorio original.
// TODO: put this in a preconstruct or preconst ESLint plugin
import fs from 'node:fs'
import path from 'node:path'
const EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js']
const EXTENSION_PATTERN = /\.[^./]+$/u
const TYPESCRIPT_EXTENSION_PATTERN = /\.(?:[cm]?ts|tsx)$/u
const requireNodeEsmImports = {
meta: {
type: 'problem',
docs: {
description: 'require relative imports that will work properly in Node.js ESM',
},
fixable: 'code',
schema: [],
messages: {
useResolved: "Use '{{resolved}}' for this relative import.",
},
},
create(context) {
const filename = getFilename(context)
const sourceCode = context.sourceCode ?? context.getSourceCode()
const directoryFiles = new Map()
function checkSource(node) {
if (!node || typeof node.value !== 'string') return
const specifier = node.value
if (!isRelativeSpecifier(specifier)) return
const resolved = resolveSpecifier(filename, specifier, directoryFiles)
if (!resolved || resolved === specifier) return
context.report({
node,
messageId: 'useResolved',
data: { resolved },
fix(fixer) {
return fixer.replaceTextRange(node.range, quoteLikeSource(sourceCode, node, resolved))
},
})
}
return {
ImportDeclaration(node) {
checkSource(node.source)
},
ExportAllDeclaration(node) {
checkSource(node.source)
},
ExportNamedDeclaration(node) {
checkSource(node.source)
},
ImportExpression(node) {
checkSource(node.source)
},
}
},
}
function getFilename(context) {
return context.filename ?? context.getFilename()
}
function isRelativeSpecifier(specifier) {
return (
specifier === '.' ||
specifier === '..' ||
specifier.startsWith('./') ||
specifier.startsWith('../')
)
}
function resolveSpecifier(importer, specifier, directoryFiles) {
const importerDir = path.dirname(importer)
const absoluteBase = path.resolve(importerDir, specifier)
if (TYPESCRIPT_EXTENSION_PATTERN.test(absoluteBase)) return specifier
if (isFile(absoluteBase, directoryFiles)) return specifier
const withoutJsExtension = stripJsExtension(absoluteBase)
const candidates = candidatePaths(withoutJsExtension)
for (const candidate of candidates) {
if (!isFile(candidate, directoryFiles)) continue
const relative = toSpecifier(path.relative(importerDir, candidate))
return relative
}
return undefined
}
function stripJsExtension(filePath) {
return filePath.endsWith('.js') || filePath.endsWith('.jsx')
? filePath.slice(0, -path.extname(filePath).length)
: filePath
}
function candidatePaths(base) {
if (hasExtension(base)) return [base]
const direct = EXTENSIONS.map(extension => base + extension)
const index = EXTENSIONS.map(extension => path.join(base, 'index' + extension))
return [...direct, ...index]
}
function hasExtension(filePath) {
return EXTENSION_PATTERN.test(path.basename(filePath))
}
function isFile(filePath, directoryFiles) {
const directory = path.dirname(filePath)
let files = directoryFiles.get(directory)
if (!files) {
try {
files = new Set(
fs
.readdirSync(directory, { withFileTypes: true })
.filter(entry => entry.isFile())
.map(entry => entry.name)
)
} catch {
files = new Set()
}
directoryFiles.set(directory, files)
}
return files.has(path.basename(filePath))
}
function toSpecifier(relativePath) {
const normalized = relativePath.split(path.sep).join('/')
return normalized.startsWith('.') ? normalized : './' + normalized
}
function quoteLikeSource(sourceCode, node, value) {
const raw = sourceCode.getText(node)
const quote = raw.startsWith("'") ? "'" : '"'
return quote + value + quote
}
export default {
meta: {
name: 'eslint-plugin-esm-imports',
},
rules: {
'node-esm-imports': requireNodeEsmImports,
},
}