structure-validation
Version:
A Node.js CLI tool for validating codebase folder and file structure using a clean declarative configuration. Part of the guardz ecosystem for comprehensive TypeScript development.
444 lines • 18.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImportUpdateService = void 0;
const ts_morph_1 = require("ts-morph");
const path_1 = __importDefault(require("path"));
const fsSync = __importStar(require("fs"));
/**
* Application service for updating import statements when files are moved
*/
class ImportUpdateService {
constructor(basePath = process.cwd()) {
this.basePath = basePath;
// Check if tsconfig.json exists, if not create a minimal one for the project
const tsConfigPath = path_1.default.join(basePath, 'tsconfig.json');
if (!fsSync.existsSync(tsConfigPath)) {
// Create a minimal tsconfig.json for the test environment
const minimalTsConfig = {
compilerOptions: {
target: "ES2020",
module: "commonjs",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true
},
include: ["**/*.ts", "**/*.tsx"],
exclude: ["node_modules", "dist", "build"]
};
fsSync.writeFileSync(tsConfigPath, JSON.stringify(minimalTsConfig, null, 2));
}
this.project = new ts_morph_1.Project({
tsConfigFilePath: tsConfigPath,
skipAddingFilesFromTsConfig: true
});
}
/**
* Update all import statements after files have been moved
*/
async updateImportsAfterMove(movedFiles) {
const updated = [];
const errors = [];
if (movedFiles.length === 0) {
return {
updated: [],
errors: [],
summary: 'No files were moved, no imports to update'
};
}
try {
// Add all TypeScript/JavaScript files to the project
await this.addSourceFiles();
// Get all source files
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
try {
const fileUpdates = await this.updateImportsInFile(sourceFile, movedFiles);
if (fileUpdates.length > 0) {
updated.push(`${sourceFile.getFilePath()}: ${fileUpdates.length} import(s) updated`);
}
}
catch (error) {
errors.push(`Failed to update imports in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Save all changes
await this.project.save();
const summary = this.generateSummary(updated, errors);
return { updated, errors, summary };
}
catch (error) {
errors.push(`Failed to update imports: ${error instanceof Error ? error.message : String(error)}`);
return { updated, errors, summary: 'Failed to update imports' };
}
}
/**
* Add all TypeScript/JavaScript files to the project
*/
async addSourceFiles() {
const sourceFiles = await this.findSourceFiles();
for (const filePath of sourceFiles) {
this.project.addSourceFileAtPath(filePath);
}
}
/**
* Find all TypeScript/JavaScript files in the project
*/
async findSourceFiles() {
const { glob } = await Promise.resolve().then(() => __importStar(require('fast-glob')));
const patterns = [
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx'
];
const excludePatterns = [
'node_modules/**',
'dist/**',
'build/**',
'.git/**',
'coverage/**',
'.structure-validation-backup/**'
];
return await glob(patterns, {
cwd: this.basePath,
ignore: excludePatterns,
absolute: true
});
}
/**
* Update imports in a specific file
*/
async updateImportsInFile(sourceFile, movedFiles) {
const updates = [];
const importDeclarations = sourceFile.getImportDeclarations();
for (const importDecl of importDeclarations) {
const moduleSpecifier = importDecl.getModuleSpecifier();
if (!moduleSpecifier)
continue;
const importPath = moduleSpecifier.getLiteralValue();
// Skip non-relative imports
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
continue;
}
// Find if this import needs to be updated
const movedFile = this.findMovedFileForImport(importPath, sourceFile.getFilePath(), movedFiles);
if (movedFile) {
const newImportPath = this.calculateNewImportPath(importPath, sourceFile.getFilePath(), movedFile);
if (newImportPath !== importPath) {
moduleSpecifier.setLiteralValue(newImportPath);
updates.push(`${importPath} → ${newImportPath}`);
}
}
}
return updates;
}
/**
* Find if an import path corresponds to a moved file
*/
findMovedFileForImport(importPath, sourceFilePath, movedFiles) {
// Resolve the absolute path of the imported file
const sourceDir = path_1.default.dirname(sourceFilePath);
const absoluteImportPath = path_1.default.resolve(sourceDir, importPath);
// Add file extensions to check
const possiblePaths = [
absoluteImportPath,
`${absoluteImportPath}.ts`,
`${absoluteImportPath}.tsx`,
`${absoluteImportPath}.js`,
`${absoluteImportPath}.jsx`,
path_1.default.join(absoluteImportPath, 'index.ts'),
path_1.default.join(absoluteImportPath, 'index.tsx'),
path_1.default.join(absoluteImportPath, 'index.js'),
path_1.default.join(absoluteImportPath, 'index.jsx')
];
for (const possiblePath of possiblePaths) {
const movedFile = movedFiles.find(mf => this.pathsMatch(mf.oldPath, possiblePath));
if (movedFile) {
return movedFile;
}
}
return null;
}
/**
* Check if two paths match (handling different separators and normalization)
*/
pathsMatch(path1, path2) {
return path_1.default.normalize(path1) === path_1.default.normalize(path2);
}
/**
* Calculate the new import path after a file has been moved
*/
calculateNewImportPath(oldImportPath, sourceFilePath, movedFile) {
const sourceDir = path_1.default.dirname(sourceFilePath);
const newFileDir = path_1.default.dirname(movedFile.newPath);
// Calculate relative path from source file to the new location
const relativePath = path_1.default.relative(sourceDir, newFileDir);
// Get the filename without extension
const filename = path_1.default.basename(movedFile.newPath, path_1.default.extname(movedFile.newPath));
// Construct the new import path
let newImportPath = path_1.default.join(relativePath, filename);
// Normalize path separators for the current OS
newImportPath = newImportPath.replace(/\\/g, '/');
// Ensure it starts with ./
if (!newImportPath.startsWith('./') && !newImportPath.startsWith('../')) {
newImportPath = `./${newImportPath}`;
}
return newImportPath;
}
/**
* Generate a summary of the import update operation
*/
generateSummary(updated, errors) {
const totalFiles = updated.length;
const totalErrors = errors.length;
if (totalFiles === 0 && totalErrors === 0) {
return 'No import statements needed updating';
}
let summary = `Import update completed:\n`;
summary += ` • Files updated: ${totalFiles}\n`;
summary += ` • Errors: ${totalErrors}\n`;
if (totalErrors > 0) {
summary += ` • Success rate: ${Math.round(((totalFiles - totalErrors) / (totalFiles + totalErrors)) * 100)}%`;
}
return summary;
}
/**
* Get statistics about the import update operation
*/
getStats() {
const sourceFiles = this.project.getSourceFiles();
let totalImports = 0;
for (const sourceFile of sourceFiles) {
totalImports += sourceFile.getImportDeclarations().length;
}
return {
totalFiles: sourceFiles.length,
totalImports
};
}
/**
* Update imports after files have been renamed
* @param renamedFiles Array of renamed file information
* @returns Promise<{ updated: string[]; errors: string[]; summary: string }>
*/
async updateImportsAfterRename(renamedFiles) {
// For renames, we can use the same logic as moves since the file structure changes
return this.updateImportsAfterMove(renamedFiles);
}
/**
* Remove imports for deleted files
* @param deletedFiles Array of deleted file paths
* @returns Promise<{ updated: string[]; errors: string[]; summary: string }>
*/
async removeImportsForDeletedFiles(deletedFiles) {
const updated = [];
const errors = [];
if (deletedFiles.length === 0) {
return {
updated: [],
errors: [],
summary: 'No files were deleted, no imports to remove'
};
}
try {
// Add all TypeScript/JavaScript files to the project
await this.addSourceFiles();
// Get all source files
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
try {
const fileUpdates = await this.removeImportsInFile(sourceFile, deletedFiles);
if (fileUpdates.length > 0) {
updated.push(`${sourceFile.getFilePath()}: ${fileUpdates.length} import(s) removed`);
}
}
catch (error) {
errors.push(`Failed to remove imports in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Save all changes
await this.project.save();
const summary = this.generateSummary(updated, errors);
return { updated, errors, summary };
}
catch (error) {
errors.push(`Failed to remove imports: ${error instanceof Error ? error.message : String(error)}`);
return { updated, errors, summary: 'Failed to remove imports' };
}
}
/**
* Remove imports for deleted files in a specific file
* @param sourceFile The source file to check
* @param deletedFiles Array of deleted file paths
* @returns Promise<string[]> Array of removed import descriptions
*/
async removeImportsInFile(sourceFile, deletedFiles) {
const updates = [];
const importDeclarations = sourceFile.getImportDeclarations();
// Work backwards to avoid index issues when removing imports
for (let i = importDeclarations.length - 1; i >= 0; i--) {
const importDecl = importDeclarations[i];
if (!importDecl)
continue;
const moduleSpecifier = importDecl.getModuleSpecifier();
if (!moduleSpecifier)
continue;
const importPath = moduleSpecifier.getLiteralValue();
// Skip non-relative imports
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
continue;
}
// Check if this import references a deleted file
const sourceDir = path_1.default.dirname(sourceFile.getFilePath());
const absoluteImportPath = path_1.default.resolve(sourceDir, importPath);
// Add file extensions to check
const possiblePaths = [
absoluteImportPath,
`${absoluteImportPath}.ts`,
`${absoluteImportPath}.tsx`,
`${absoluteImportPath}.js`,
`${absoluteImportPath}.jsx`,
path_1.default.join(absoluteImportPath, 'index.ts'),
path_1.default.join(absoluteImportPath, 'index.tsx'),
path_1.default.join(absoluteImportPath, 'index.js'),
path_1.default.join(absoluteImportPath, 'index.jsx')
];
const isDeletedFile = possiblePaths.some(possiblePath => deletedFiles.some(deletedFile => this.pathsMatch(deletedFile, possiblePath)));
if (isDeletedFile) {
// Remove the import declaration
importDecl.remove();
updates.push(`Removed import: ${importPath}`);
}
}
return updates;
}
/**
* Validate import paths and suggest fixes
* @returns Promise<{ invalid: string[]; suggestions: string[] }>
*/
async validateImportPaths() {
const invalid = [];
const suggestions = [];
try {
// Add all TypeScript/JavaScript files to the project
await this.addSourceFiles();
// Get all source files
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
const importDeclarations = sourceFile.getImportDeclarations();
for (const importDecl of importDeclarations) {
const moduleSpecifier = importDecl.getModuleSpecifier();
if (!moduleSpecifier)
continue;
const importPath = moduleSpecifier.getLiteralValue();
// Skip non-relative imports
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
continue;
}
// Check if the imported file exists
const sourceDir = path_1.default.dirname(sourceFile.getFilePath());
const absoluteImportPath = path_1.default.resolve(sourceDir, importPath);
const possiblePaths = [
absoluteImportPath,
`${absoluteImportPath}.ts`,
`${absoluteImportPath}.tsx`,
`${absoluteImportPath}.js`,
`${absoluteImportPath}.jsx`,
path_1.default.join(absoluteImportPath, 'index.ts'),
path_1.default.join(absoluteImportPath, 'index.tsx'),
path_1.default.join(absoluteImportPath, 'index.js'),
path_1.default.join(absoluteImportPath, 'index.jsx')
];
const fileExists = possiblePaths.some(possiblePath => fsSync.existsSync(possiblePath));
if (!fileExists) {
invalid.push(`${sourceFile.getFilePath()}: ${importPath}`);
// Try to find a similar file
const suggestedPath = await this.findSimilarFile(absoluteImportPath);
if (suggestedPath) {
const relativePath = path_1.default.relative(sourceDir, suggestedPath);
const normalizedPath = relativePath.replace(/\\/g, '/');
suggestions.push(`${sourceFile.getFilePath()}: ${importPath} → ${normalizedPath}`);
}
}
}
}
}
catch (error) {
invalid.push(`Failed to validate imports: ${error instanceof Error ? error.message : String(error)}`);
}
return { invalid, suggestions };
}
/**
* Find a similar file when the original import path is invalid
* @param invalidPath The invalid import path
* @returns Promise<string | null> The path to a similar file, or null if not found
*/
async findSimilarFile(invalidPath) {
try {
const { glob } = await Promise.resolve().then(() => __importStar(require('fast-glob')));
const baseDir = path_1.default.dirname(invalidPath);
const fileName = path_1.default.basename(invalidPath, path_1.default.extname(invalidPath));
// Search for files with similar names
const patterns = [
`${fileName}.*`,
`${fileName}*.ts`,
`${fileName}*.tsx`,
`${fileName}*.js`,
`${fileName}*.jsx`
];
for (const pattern of patterns) {
const files = await glob(pattern, {
cwd: baseDir,
absolute: true,
onlyFiles: true
});
if (files.length > 0) {
return files[0] || null;
}
}
return null;
}
catch (error) {
return null;
}
}
}
exports.ImportUpdateService = ImportUpdateService;
//# sourceMappingURL=ImportUpdateService.js.map