Spaces:
Build error
Build error
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; | |
}, | |
}); | |
} | |
}, | |
}; | |
}, | |
}; | |