File size: 5,081 Bytes
7357f85 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
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; // Default to true
const aliases = options.aliases || {};
// Get the project root directory
const projectRoot = process.cwd();
// Utility function to resolve file paths
function resolveImportPath(importPath, currentFilePath) {
// Handle relative paths
if (importPath.startsWith("./") || importPath.startsWith("../")) {
return path.resolve(path.dirname(currentFilePath), importPath);
}
// Handle aliased paths
for (const [alias, aliasPath] of Object.entries(aliases)) {
// Check if the import starts with this alias
if (importPath === alias || importPath.startsWith(`${alias}/`)) {
// Replace the alias with the actual path
const relativePath = importPath === alias ? "" : importPath.slice(alias.length + 1); // +1 for the slash
// Convert the aliasPath to an absolute path
let absoluteAliasPath = aliasPath;
if (!path.isAbsolute(absoluteAliasPath)) {
absoluteAliasPath = path.resolve(projectRoot, aliasPath);
}
return path.join(absoluteAliasPath, relativePath);
}
}
return null;
}
// Find the file extension by checking which file exists
function findActualFile(basePath) {
if (!basePath) return null;
try {
// Get the directory and base name
const dir = path.dirname(basePath);
const base = path.basename(basePath);
// If the directory doesn't exist, return early
if (!fs.existsSync(dir)) {
return null;
}
// Read all files in the directory
const files = fs.readdirSync(dir);
// Look for files that match our base name plus any extension
for (const file of files) {
const fileParts = path.parse(file);
// If we find a file that matches our base name
if (fileParts.name === base) {
// Handle TypeScript to JavaScript conversion
if (tsToJs && fileParts.ext === ".ts") {
return {
actualPath: path.join(dir, file),
importExt: ".js", // Import as .js even though it's a .ts file
};
}
// Otherwise use the actual extension
return {
actualPath: path.join(dir, file),
importExt: fileParts.ext,
};
}
}
} catch (error) {
// If there's an error checking file existence, return null
console.error("Error checking files:", error);
}
return null;
}
return {
ImportDeclaration(node) {
const source = node.source.value;
// Check if it's a relative import or matches a manually specified include path
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));
// Skip if it's not a relative import, aliased path, or included path
if (!isRelativeImport && !isAliasedPath && !isIncludedPath) {
return;
}
// Skip ignored paths
if (ignorePaths.some(path => source.includes(path))) {
return;
}
// Check if the import already has an extension
const hasExtension = path.extname(source) !== "";
if (!hasExtension) {
// Get current file path to resolve the import
const currentFilePath = context.getFilename();
// Try to determine the correct file by checking what exists
const resolvedPath = resolveImportPath(source, currentFilePath);
const fileInfo = findActualFile(resolvedPath);
context.report({
node,
messageId: fileInfo ? "missingExtension" : "noFileFound",
fix(fixer) {
// Only provide a fix if we found a file
if (fileInfo) {
// Replace the string literal with one that includes the extension
return fixer.replaceText(node.source, `"${source}${fileInfo.importExt}"`);
}
// Otherwise, don't try to fix
return null;
},
});
}
},
};
},
};
|