UNPKG

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
"use strict"; 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