path-validator-cli
Version:
A CLI tool to validate and fix broken paths in a project. Prevent deployment issues by validating and correcting paths directly in your codebase.
140 lines (117 loc) • 5.8 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { searchPaths } from './searchFiles.js';
import { parseHTML, parseCSS, parseJS, parsePHP } from '../utils/parsedContent.js';
import { errorMessages } from '../cli/messages.js';
export async function validatePaths(projectRoot) {
const allProjectFiles = await searchPaths(projectRoot);
const projectFilePaths = allProjectFiles.map(file => file.path);
const searchedFiles = allProjectFiles.filter(file => ['.html', '.css', '.js', '.php'].includes(file.extension));
const validPaths = [];
const invalidPaths = [];
const parsers = {
'.html': parseHTML,
'.css': parseCSS,
'.js': parseJS,
'.php': parsePHP
};
const seenInvalidPaths = new Set();
for (const file of searchedFiles) {
const content = await fs.promises.readFile(file.path, 'utf-8');
const lines = content.split('\n');
const parser = parsers[file.extension];
if (parser) {
const extractedPaths = parser(content, file.path, projectRoot);
extractedPaths.invalidMatches.forEach(pathData => {
const importPath = pathData.path;
const fileDir = path.dirname(file.path);
let fullPath = path.resolve(fileDir, importPath);
let issueType = null;
let dynamicSuggestion = "";
// Avoid duplicated path flagging
const uniqueKey = `${file.path}:${importPath}`;
if (seenInvalidPaths.has(uniqueKey)) {
return; // Exit if already flagged once
}
seenInvalidPaths.add(uniqueKey);
// Find the correct line number for each issue
const lineNumber = lines.findIndex(line => line.includes(importPath)) + 1;
if (importPath.startsWith('./')) {
const fullRelativePath = path.resolve(fileDir, importPath);
if (fs.existsSync(fullRelativePath)) {
validPaths.push({
path: importPath,
sourceFile: file.path,
lineNumber
});
return; // Return if it's already correctly relative
}
}
// Identify possible path problems
if (importPath.startsWith('/')) {
issueType = "absolutePath";
let cleanPath = importPath.replace(/^\/+/, "");
let fileDirectory = path.dirname(file.path);
let parentDir = path.dirname(fileDirectory);
while (parentDir !== projectRoot && path.basename(parentDir) !== path.basename(path.dirname(fileDirectory))) {
parentDir = path.dirname(parentDir);
}
let targetPath = path.join(parentDir, cleanPath);
if (!fs.existsSync(targetPath)) {
targetPath = path.join(projectRoot, cleanPath);
}
let relativePath = path.relative(fileDirectory, targetPath);
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
dynamicSuggestion = errorMessages[issueType].suggestion(relativePath);
} else if (!fs.existsSync(fullPath)) {
issueType = "missingFile";
const correctRelativePath = findCorrectPath(importPath, projectFilePaths, fileDir);
dynamicSuggestion = errorMessages[issueType].suggestion(correctRelativePath);
} else if (importPath.startsWith('..') && !fullPath.startsWith(projectRoot)) {
issueType = "tooManyBack";
dynamicSuggestion = errorMessages[issueType].suggestion;
} else if (importPath.startsWith("../")) {
issueType = "incorrectRelative";
dynamicSuggestion = errorMessages[issueType].suggestion;
} else {
issueType = "unknownPath";
dynamicSuggestion = errorMessages[issueType].suggestion;
}
if (issueType) {
invalidPaths.push({
...pathData,
issue: issueType,
suggestion: dynamicSuggestion,
lineNumber
});
}
});
}
}
return { validPaths, invalidPaths };
}
function findCorrectPath(brokenPath, projectFilePaths, fileDir) {
const filename = path.basename(brokenPath);
const possibleMatches = projectFilePaths.filter(filePath => filePath.endsWith(filename));
if (possibleMatches.length === 1) {
return path.relative(fileDir, possibleMatches[0]); // Return correct relative path
}
if (possibleMatches.length > 1) {
// Find closest match based on path
const closestMatch = possibleMatches
.map(match => ({
path: match,
distance: path.relative(fileDir, match).split(path.sep).length
}))
.sort((a, b) => a.distance - b.distance)[0].path;
let relativePath = path.relative(fileDir, closestMatch);
// Avoid unnecessary use of ../
if (!relativePath.startsWith('../') && !relativePath.startsWith('./')) {
relativePath = `./${relativePath}`;
}
return relativePath;
}
return null; // If no match was found return null
}