|
import fs from "fs"; |
|
import path from "path"; |
|
|
|
export default { |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "Enforce file extensions in import statements", |
|
}, |
|
fixable: "code", |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
ignorePaths: { |
|
type: "array", |
|
items: { type: "string" }, |
|
}, |
|
includePaths: { |
|
type: "array", |
|
items: { type: "string" }, |
|
description: "Path patterns to include (e.g., '$lib/')", |
|
}, |
|
tsToJs: { |
|
type: "boolean", |
|
description: "Convert .ts files to .js when importing", |
|
}, |
|
aliases: { |
|
type: "object", |
|
description: "Map of path aliases to their actual paths (e.g., {'$lib': 'src/lib'})", |
|
}, |
|
}, |
|
additionalProperties: false, |
|
}, |
|
], |
|
messages: { |
|
missingExtension: "Import should include a file extension", |
|
noFileFound: "Import is missing extension and no matching file was found", |
|
}, |
|
}, |
|
create(context) { |
|
const options = context.options[0] || {}; |
|
const ignorePaths = options.ignorePaths || []; |
|
const includePaths = options.includePaths || []; |
|
const tsToJs = options.tsToJs !== undefined ? options.tsToJs : true; |
|
const aliases = options.aliases || {}; |
|
|
|
|
|
const projectRoot = process.cwd(); |
|
|
|
|
|
function resolveImportPath(importPath, currentFilePath) { |
|
|
|
if (importPath.startsWith("./") || importPath.startsWith("../")) { |
|
return path.resolve(path.dirname(currentFilePath), importPath); |
|
} |
|
|
|
|
|
for (const [alias, aliasPath] of Object.entries(aliases)) { |
|
|
|
if (importPath === alias || importPath.startsWith(`${alias}/`)) { |
|
|
|
const relativePath = importPath === alias ? "" : importPath.slice(alias.length + 1); |
|
|
|
|
|
let absoluteAliasPath = aliasPath; |
|
if (!path.isAbsolute(absoluteAliasPath)) { |
|
absoluteAliasPath = path.resolve(projectRoot, aliasPath); |
|
} |
|
|
|
return path.join(absoluteAliasPath, relativePath); |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
|
|
function findActualFile(basePath) { |
|
if (!basePath) return null; |
|
|
|
try { |
|
|
|
const dir = path.dirname(basePath); |
|
const base = path.basename(basePath); |
|
|
|
|
|
if (!fs.existsSync(dir)) { |
|
return null; |
|
} |
|
|
|
|
|
const files = fs.readdirSync(dir); |
|
|
|
|
|
for (const file of files) { |
|
const fileParts = path.parse(file); |
|
|
|
|
|
if (fileParts.name === base) { |
|
|
|
if (tsToJs && fileParts.ext === ".ts") { |
|
return { |
|
actualPath: path.join(dir, file), |
|
importExt: ".js", |
|
}; |
|
} |
|
|
|
|
|
return { |
|
actualPath: path.join(dir, file), |
|
importExt: fileParts.ext, |
|
}; |
|
} |
|
} |
|
} catch (error) { |
|
|
|
console.error("Error checking files:", error); |
|
} |
|
|
|
return null; |
|
} |
|
|
|
return { |
|
ImportDeclaration(node) { |
|
const source = node.source.value; |
|
|
|
|
|
const isRelativeImport = source.startsWith("./") || source.startsWith("../"); |
|
const isAliasedPath = Object.keys(aliases).some(alias => source === alias || source.startsWith(`${alias}/`)); |
|
const isIncludedPath = includePaths.some(pattern => source.startsWith(pattern)); |
|
|
|
|
|
if (!isRelativeImport && !isAliasedPath && !isIncludedPath) { |
|
return; |
|
} |
|
|
|
|
|
if (ignorePaths.some(path => source.includes(path))) { |
|
return; |
|
} |
|
|
|
|
|
const hasExtension = path.extname(source) !== ""; |
|
if (!hasExtension) { |
|
|
|
const currentFilePath = context.getFilename(); |
|
|
|
|
|
const resolvedPath = resolveImportPath(source, currentFilePath); |
|
const fileInfo = findActualFile(resolvedPath); |
|
|
|
context.report({ |
|
node, |
|
messageId: fileInfo ? "missingExtension" : "noFileFound", |
|
fix(fixer) { |
|
|
|
if (fileInfo) { |
|
|
|
return fixer.replaceText(node.source, `"${source}${fileInfo.importExt}"`); |
|
} |
|
|
|
|
|
return null; |
|
}, |
|
}); |
|
} |
|
}, |
|
}; |
|
}, |
|
}; |
|
|