frontend-standards-checker
Version:
A comprehensive frontend standards validation tool with TypeScript support
449 lines (448 loc) • 17.1 kB
JavaScript
"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;