UNPKG

frontend-standards-checker

Version:

A comprehensive frontend standards validation tool with TypeScript support

449 lines (448 loc) 17.1 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.ProjectAnalyzer = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); /** * Analyzes a project's structure, type, and zones (sub-projects/packages). * Supports monorepo detection and zone extraction for various JS/TS project types. * * @remarks * - Detects project type using `package.json` and heuristics. * - Supports monorepo setups (Yarn/NPM workspaces, Nx, TurboRepo, Lerna, Rush). * - Extracts zones from standard directories, workspaces, and custom configuration. * - Provides structure validation and naming convention checks via additional validators. * * @example * ```typescript * const analyzer = new ProjectAnalyzer('/path/to/project', logger); * const analysis = await analyzer.analyze({ includePackages: true }); * ``` * * @public */ class ProjectAnalyzer { 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_1.default.join(projectPath, 'package.json'); const hasPackageJson = fs_1.default.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_1.default.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_1.default.existsSync(path_1.default.join(projectPath, 'src')); const hasPages = fs_1.default.existsSync(path_1.default.join(projectPath, 'pages')); const hasApp = fs_1.default.existsSync(path_1.default.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_1.default.existsSync(path_1.default.join(this.rootDir, marker))); if (hasStandardMarkers) { return true; } // Check for workspaces in package.json const packageJsonPath = path_1.default.join(this.rootDir, 'package.json'); if (fs_1.default.existsSync(packageJsonPath)) { try { const packageJsonContent = fs_1.default.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_1.default.join(this.rootDir, zoneName); if (!fs_1.default.existsSync(candidatePath) || !fs_1.default.statSync(candidatePath).isDirectory()) { return zones; } const subDirs = fs_1.default .readdirSync(candidatePath) .map((dir) => path_1.default.join(candidatePath, dir)) .filter((dirPath) => fs_1.default.statSync(dirPath).isDirectory()); for (const subDir of subDirs) { zones.push({ name: path_1.default.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_1.default.join(this.rootDir, customZone); if (fs_1.default.existsSync(customZonePath) && fs_1.default.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_1.default.join(this.rootDir, 'package.json'); if (!fs_1.default.existsSync(packageJsonPath)) { return zones; } try { const packageJsonContent = fs_1.default.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_1.default.join(this.rootDir, pattern); if (fs_1.default.existsSync(workspacePath) && fs_1.default.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_1.default.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 Promise.resolve().then(() => __importStar(require('./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); } } exports.ProjectAnalyzer = ProjectAnalyzer;