| 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; |
| }, |
| }); |
| } |
| }, |
| }; |
| }, |
| }; |
|
|