Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| 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; | |
| }, | |
| }); | |
| } | |
| }, | |
| }; | |
| }, | |
| }; | |