UNPKG

frontend-standards-checker

Version:

A comprehensive frontend standards validation tool with TypeScript support

396 lines 14.5 kB
import fs from 'fs'; import path from 'path'; /** * Project analyzer for detecting project type, structure, and zones */ export class ProjectAnalyzer { rootDir; logger; constructor(rootDir, logger) { this.rootDir = rootDir; this.logger = logger; } /** * Analyze the project structure and return project information */ async analyze(config = {}) { const projectInfo = { type: this.detectProjectType(), projectType: this.detectProjectType(), // Alias for backwards compatibility isMonorepo: this.isMonorepo(), zones: [], structure: {}, rootPath: this.rootDir, }; if (projectInfo.isMonorepo) { projectInfo.zones = await this.detectMonorepoZones(config); } else if (config.customZones && config.customZones.length > 0) { // For non-monorepo projects, use custom zones if defined projectInfo.zones = this.processCustomZones(config.customZones); } else { // Only use root zone if no custom zones are defined projectInfo.zones = [ { name: '.', path: this.rootDir, type: projectInfo.type, }, ]; } // Remove duplicate zones based on name projectInfo.zones = projectInfo.zones.filter((zone, index, self) => index === self.findIndex((z) => z.name === zone.name)); this.logger.debug('Project analysis result:', projectInfo); return projectInfo; } /** * Detect the type of project (app, package, library, etc.) */ detectProjectType(projectPath = this.rootDir) { const packageJsonPath = path.join(projectPath, 'package.json'); const hasPackageJson = fs.existsSync(packageJsonPath); if (hasPackageJson) { const projectType = this.detectProjectTypeFromPackageJson(packageJsonPath); if (projectType) return projectType; } return this.detectProjectTypeFromHeuristics(projectPath, hasPackageJson); } /** * Detect project type from package.json dependencies */ detectProjectTypeFromPackageJson(packageJsonPath) { try { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonContent); // Check for Next.js app if (packageJson.dependencies?.next || packageJson.devDependencies?.next) { return 'next'; } // Check for React app if (packageJson.dependencies?.react || packageJson.devDependencies?.react) { return 'react'; } // Check for Angular if (packageJson.dependencies?.['@angular/core'] || packageJson.devDependencies?.['@angular/core']) { return 'angular'; } // Check for Vue if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) { return 'vue'; } // Check for Node.js package if (packageJson.main || packageJson.exports) { return 'node'; } return null; } catch (error) { this.logger.warn('Failed to parse package.json:', error.message); return null; } } /** * Detect project type using heuristics */ detectProjectTypeFromHeuristics(projectPath, hasPackageJson) { const hasSrc = fs.existsSync(path.join(projectPath, 'src')); const hasPages = fs.existsSync(path.join(projectPath, 'pages')); const hasApp = fs.existsSync(path.join(projectPath, 'app')); if (hasPages || hasApp) return 'next'; if (hasPackageJson && hasSrc) return 'node'; return 'generic'; } /** * Detect zone type for a specific path */ detectZoneType(zonePath) { return this.detectProjectType(zonePath); } /** * Check if the project is a monorepo */ isMonorepo() { const monorepoMarkers = [ 'packages', 'apps', 'lerna.json', 'turbo.json', 'nx.json', 'rush.json', ]; // Check for standard monorepo markers (directories and config files) const hasStandardMarkers = monorepoMarkers.some((marker) => fs.existsSync(path.join(this.rootDir, marker))); if (hasStandardMarkers) { return true; } // Check for workspaces in package.json const packageJsonPath = path.join(this.rootDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonContent); // Check for workspaces property (yarn/npm workspaces) if (packageJson.workspaces) { return true; } } catch (error) { // Ignore JSON parsing errors - if package.json is malformed, treat as non-monorepo this.logger.debug('Failed to parse package.json for monorepo detection:', error); } } return false; } /** * Detect zones in a monorepo */ async detectMonorepoZones(zoneConfig = {}) { const zones = []; // If onlyZone is specified, process only that zone if (zoneConfig.onlyZone) { zones.push(...this.processZoneDirectory(zoneConfig.onlyZone)); return zones; } // Process standard zones const standardZones = this.getStandardZones(zoneConfig); for (const zoneName of standardZones) { zones.push(...this.processZoneDirectory(zoneName)); } // Process workspaces from package.json zones.push(...this.processWorkspaceZones(zoneConfig)); // Process custom zones zones.push(...this.processCustomZones(zoneConfig.customZones)); return zones; } /** * Get list of standard zones to process based on configuration */ getStandardZones(zoneConfig) { const zones = ['apps', 'libs', 'projects']; // Always include these if (zoneConfig.includePackages === true) { zones.push('packages'); // Only include if explicitly enabled } return zones; } /** * Process a zone directory and return its sub-zones */ processZoneDirectory(zoneName) { const zones = []; const candidatePath = path.join(this.rootDir, zoneName); if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isDirectory()) { return zones; } const subDirs = fs .readdirSync(candidatePath) .map((dir) => path.join(candidatePath, dir)) .filter((dirPath) => fs.statSync(dirPath).isDirectory()); for (const subDir of subDirs) { zones.push({ name: path.relative(this.rootDir, subDir), path: subDir, type: this.detectZoneType(subDir), }); } return zones; } /** * Process custom zones from configuration */ processCustomZones(customZones) { const zones = []; if (!Array.isArray(customZones)) { return zones; } for (const customZone of customZones) { const customZonePath = path.join(this.rootDir, customZone); if (fs.existsSync(customZonePath) && fs.statSync(customZonePath).isDirectory()) { zones.push({ name: customZone, path: customZonePath, type: this.detectZoneType(customZonePath), }); } } return zones; } /** * Process workspace zones from package.json */ processWorkspaceZones(_zoneConfig) { const zones = []; const packageJsonPath = path.join(this.rootDir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return zones; } try { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonContent); if (!packageJson.workspaces) { return zones; } // Handle both array and object formats for workspaces let workspacePatterns = []; if (Array.isArray(packageJson.workspaces)) { workspacePatterns = packageJson.workspaces; } else if (packageJson.workspaces.packages) { workspacePatterns = packageJson.workspaces.packages; } // Process workspace patterns to find actual directories for (const pattern of workspacePatterns) { // For simple directory names (not glob patterns) if (!pattern.includes('*') && !pattern.includes('?')) { const workspacePath = path.join(this.rootDir, pattern); if (fs.existsSync(workspacePath) && fs.statSync(workspacePath).isDirectory()) { zones.push({ name: pattern, path: workspacePath, type: this.detectZoneType(workspacePath), }); } } // Note: Glob pattern support could be added here if needed for complex workspace configurations } } catch (error) { this.logger.debug('Failed to process workspace zones from package.json:', error); } return zones; } /** * Get expected structure for a project type */ getExpectedStructure(projectType) { const structures = { next: ['app', 'components', 'public', 'src'], react: ['src', 'public'], angular: ['src', 'e2e'], vue: ['src', 'public'], node: ['src', 'package.json', 'lib'], generic: [], }; return structures[projectType] ?? structures.generic ?? []; } /** * Get expected src structure */ getExpectedSrcStructure() { return { assets: [], components: ['index.ts'], constants: ['index.ts'], modules: [], helpers: ['index.ts'], hooks: ['index.ts'], providers: ['index.ts'], styles: ['index.ts'], store: [ 'reducers', 'types', 'state.selector.ts', 'state.interface.ts', 'store', ], }; } /** * Validate zone structure and naming conventions */ async validateZoneStructure(files, directories, _zoneName) { const errors = []; const additionalValidators = await this.loadAdditionalValidators(); if (!additionalValidators) { this.logger.warn('Additional validators not available, skipping extended validation'); return errors; } try { errors.push(...(await this.validateFiles(files, additionalValidators))); errors.push(...this.validateDirectories(directories, additionalValidators)); } catch (error) { this.logger.warn('Validation error:', error.message); } return errors; } async validateFiles(files, validators) { const errors = []; const { checkNamingConventions, checkComponentFunctionNameMatch } = validators; for (const filePath of files) { errors.push(...this.validateSingleFile(filePath, checkNamingConventions, checkComponentFunctionNameMatch)); } return errors; } validateSingleFile(filePath, namingValidator, functionNameValidator) { const errors = []; const namingError = namingValidator(filePath); if (namingError) { errors.push(namingError); } if (this.isComponentIndexFile(filePath)) { this.validateComponentFunctionName(filePath, functionNameValidator, errors); } return errors; } isComponentIndexFile(filePath) { return filePath.endsWith('index.tsx') && filePath.includes('/components/'); } validateComponentFunctionName(filePath, validator, errors) { try { const content = fs.readFileSync(filePath, 'utf-8'); const functionNameError = validator(content, filePath); if (functionNameError) { errors.push(functionNameError); } } catch (error) { this.logger.warn(`Could not read file for function name validation: ${filePath}. Error: ${error.message}`); throw error; } } validateDirectories(directories, validators) { const errors = []; const { checkDirectoryNaming, checkComponentStructure } = validators; for (const dirPath of directories) { errors.push(...checkDirectoryNaming(dirPath)); if (this.isComponentDirectory(dirPath)) { errors.push(...checkComponentStructure(dirPath)); } } return errors; } isComponentDirectory(dirPath) { return (dirPath.includes('/components/') || dirPath.includes('\\components\\')); } /** * Safely load additional validators */ async loadAdditionalValidators() { try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - Temporary workaround for JS module import return await import('./additional-validators.js'); } catch (error) { this.logger.debug('Additional validators not found:', error.message); return null; } } /** * Legacy method for compatibility - not used in new implementation */ async detectZones() { const result = await this.analyze(); return result.zones.map((zone) => zone.name); } } //# sourceMappingURL=project-analyzer.js.map