vineguard-mcp-server-standalone
Version:
VineGuard MCP Server v2.1 - Intelligent QA Workflow System with advanced test generation for Jest/RTL, Cypress, and Playwright. Features smart project analysis, progressive testing strategies, and comprehensive quality patterns for React/Vue/Angular proje
495 lines • 19.5 kB
JavaScript
/**
* Configuration Analyzer for VineGuard
* Deep analysis and optimization of testing framework configurations
*/
import fs from 'fs/promises';
import path from 'path';
export class ConfigAnalyzer {
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot;
}
/**
* Analyze all testing framework configurations
*/
async analyzeAllConfigs() {
const configFiles = [
{ file: 'jest.config.js', framework: 'jest' },
{ file: 'jest.config.ts', framework: 'jest' },
{ file: 'jest.config.json', framework: 'jest' },
{ file: 'playwright.config.ts', framework: 'playwright' },
{ file: 'playwright.config.js', framework: 'playwright' },
{ file: 'cypress.config.js', framework: 'cypress' },
{ file: 'cypress.config.ts', framework: 'cypress' },
{ file: 'vitest.config.ts', framework: 'vitest' },
{ file: 'vitest.config.js', framework: 'vitest' },
{ file: 'vite.config.ts', framework: 'vitest' }, // Vitest can be in vite config
];
const analyses = [];
for (const { file, framework } of configFiles) {
const analysis = await this.analyzeConfig(file, framework);
if (analysis.exists || await this.shouldHaveConfig(framework)) {
analyses.push(analysis);
}
}
return analyses;
}
/**
* Analyze a specific configuration file
*/
async analyzeConfig(filename, framework) {
const filePath = path.join(this.projectRoot, filename);
const analysis = {
file: filename,
framework,
exists: false,
valid: false,
score: 0,
issues: [],
optimizations: [],
bestPractices: []
};
try {
await fs.access(filePath);
analysis.exists = true;
const content = await fs.readFile(filePath, 'utf8');
await this.analyzeConfigContent(analysis, content);
}
catch (error) {
analysis.exists = false;
// Check if this framework should have a config
if (await this.shouldHaveConfig(framework)) {
analysis.issues.push({
severity: 'warning',
message: `Missing ${framework} configuration file`,
suggestion: `Create ${filename} for optimal ${framework} configuration`
});
}
}
// Calculate overall score
analysis.score = this.calculateConfigScore(analysis);
return analysis;
}
/**
* Analyze configuration file content
*/
async analyzeConfigContent(analysis, content) {
try {
let config;
// Parse configuration based on file type
if (analysis.file.endsWith('.json')) {
config = JSON.parse(content);
}
else {
// For .js and .ts files, do pattern matching
config = this.extractConfigFromCode(content);
}
analysis.valid = true;
// Framework-specific analysis
switch (analysis.framework) {
case 'jest':
this.analyzeJestConfig(analysis, config, content);
break;
case 'playwright':
this.analyzePlaywrightConfig(analysis, config, content);
break;
case 'cypress':
this.analyzeCypressConfig(analysis, config, content);
break;
case 'vitest':
this.analyzeVitestConfig(analysis, config, content);
break;
}
}
catch (error) {
analysis.valid = false;
analysis.issues.push({
severity: 'error',
message: `Failed to parse configuration: ${error instanceof Error ? error.message : 'Unknown error'}`,
suggestion: 'Fix syntax errors in configuration file'
});
}
}
/**
* Analyze Jest configuration
*/
analyzeJestConfig(analysis, config, content) {
// Check for test environment
if (!this.hasConfigField(config, content, 'testEnvironment')) {
analysis.issues.push({
severity: 'warning',
message: 'testEnvironment not specified',
field: 'testEnvironment',
suggestion: 'Specify testEnvironment (jsdom for browser, node for server)'
});
}
// Check for coverage configuration
if (!this.hasConfigField(config, content, 'collectCoverageFrom')) {
analysis.optimizations.push({
type: 'coverage',
description: 'Configure coverage collection patterns',
implementation: 'Add collectCoverageFrom array with file patterns',
impact: 'high'
});
}
// Check for coverage thresholds
if (!this.hasConfigField(config, content, 'coverageThreshold')) {
analysis.optimizations.push({
type: 'coverage',
description: 'Set coverage thresholds for quality gates',
implementation: 'Add coverageThreshold with branch, function, line, statement targets',
impact: 'high'
});
}
// Check for setup files
if (!this.hasConfigField(config, content, 'setupFilesAfterEnv')) {
analysis.issues.push({
severity: 'info',
message: 'No setup files configured',
suggestion: 'Consider adding setupFilesAfterEnv for global test setup'
});
}
// Check for module name mapping (common in React projects)
if (!this.hasConfigField(config, content, 'moduleNameMapping')) {
analysis.optimizations.push({
type: 'maintainability',
description: 'Configure module name mapping for assets',
implementation: 'Add moduleNameMapping for CSS and asset imports',
impact: 'medium'
});
}
// Best practices
analysis.bestPractices.push({
category: 'Performance',
description: 'Use maxWorkers for parallel execution',
implemented: this.hasConfigField(config, content, 'maxWorkers'),
recommendation: 'Set maxWorkers to optimize test execution speed'
}, {
category: 'Debugging',
description: 'Configure verbose output for CI',
implemented: this.hasConfigField(config, content, 'verbose'),
recommendation: 'Set verbose: true in CI environments'
}, {
category: 'Isolation',
description: 'Clear mocks between tests',
implemented: this.hasConfigField(config, content, 'clearMocks'),
recommendation: 'Enable clearMocks for better test isolation'
});
}
/**
* Analyze Playwright configuration
*/
analyzePlaywrightConfig(analysis, config, content) {
// Check for base URL
if (!this.hasConfigField(config, content, 'baseURL')) {
analysis.issues.push({
severity: 'warning',
message: 'baseURL not configured',
field: 'baseURL',
suggestion: 'Set baseURL for consistent test execution'
});
}
// Check for projects (multi-browser testing)
if (!this.hasConfigField(config, content, 'projects')) {
analysis.optimizations.push({
type: 'coverage',
description: 'Configure multi-browser testing',
implementation: 'Add projects array with different browser configurations',
impact: 'high'
});
}
// Check for web server configuration
if (!this.hasConfigField(config, content, 'webServer')) {
analysis.issues.push({
severity: 'info',
message: 'webServer not configured',
suggestion: 'Configure webServer to automatically start development server'
});
}
// Check for trace configuration
if (!this.hasConfigField(config, content, 'trace')) {
analysis.optimizations.push({
type: 'maintainability',
description: 'Enable trace for debugging failures',
implementation: 'Set trace: "on-first-retry" for debugging',
impact: 'medium'
});
}
// Best practices
analysis.bestPractices.push({
category: 'Performance',
description: 'Configure parallel execution',
implemented: this.hasConfigField(config, content, 'fullyParallel'),
recommendation: 'Enable fullyParallel for faster test execution'
}, {
category: 'Reliability',
description: 'Configure retries for flaky tests',
implemented: this.hasConfigField(config, content, 'retries'),
recommendation: 'Set retries for CI environments to handle flaky tests'
}, {
category: 'Reporting',
description: 'Configure HTML reporter',
implemented: this.hasConfigField(config, content, 'reporter'),
recommendation: 'Use HTML reporter for detailed test results'
});
}
/**
* Analyze Cypress configuration
*/
analyzeCypressConfig(analysis, config, content) {
// Check for base URL
if (!this.hasConfigField(config, content, 'baseUrl')) {
analysis.issues.push({
severity: 'warning',
message: 'baseUrl not configured',
field: 'baseUrl',
suggestion: 'Set baseUrl for consistent test execution'
});
}
// Check for E2E configuration
if (!this.hasConfigField(config, content, 'e2e')) {
analysis.issues.push({
severity: 'warning',
message: 'e2e configuration missing',
suggestion: 'Configure e2e settings for end-to-end testing'
});
}
// Check for component testing
if (!this.hasConfigField(config, content, 'component')) {
analysis.optimizations.push({
type: 'coverage',
description: 'Configure component testing',
implementation: 'Add component testing configuration',
impact: 'medium'
});
}
// Best practices
analysis.bestPractices.push({
category: 'Performance',
description: 'Configure video recording selectively',
implemented: this.hasConfigField(config, content, 'video'),
recommendation: 'Disable video in development, enable in CI'
}, {
category: 'Debugging',
description: 'Configure screenshot capture',
implemented: this.hasConfigField(config, content, 'screenshotOnRunFailure'),
recommendation: 'Enable screenshots on failure for debugging'
});
}
/**
* Analyze Vitest configuration
*/
analyzeVitestConfig(analysis, config, content) {
// Check for test environment
if (!this.hasConfigField(config, content, 'environment')) {
analysis.issues.push({
severity: 'warning',
message: 'test environment not specified',
suggestion: 'Specify test environment (jsdom for browser-like, node for server)'
});
}
// Check for coverage configuration
if (!this.hasConfigField(config, content, 'coverage')) {
analysis.optimizations.push({
type: 'coverage',
description: 'Configure code coverage reporting',
implementation: 'Add coverage configuration with provider and thresholds',
impact: 'high'
});
}
// Best practices
analysis.bestPractices.push({
category: 'Performance',
description: 'Enable globals for easier testing',
implemented: this.hasConfigField(config, content, 'globals'),
recommendation: 'Enable globals to avoid importing test functions'
}, {
category: 'Developer Experience',
description: 'Configure CSS handling',
implemented: this.hasConfigField(config, content, 'css'),
recommendation: 'Enable CSS processing for component tests'
});
}
/**
* Check if a configuration field exists
*/
hasConfigField(config, content, field) {
// First check parsed config object
if (config && this.deepHasProperty(config, field)) {
return true;
}
// Fallback to string search in content
return content.includes(field);
}
/**
* Deep property check for nested config objects
*/
deepHasProperty(obj, path) {
if (!obj || typeof obj !== 'object')
return false;
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current[key] === undefined)
return false;
current = current[key];
}
return true;
}
/**
* Extract configuration from JavaScript/TypeScript code
*/
extractConfigFromCode(content) {
try {
// Simple pattern matching for common config patterns
const config = {};
// Look for common patterns
const patterns = [
/testEnvironment:\s*['"]([^'"]+)['"]/,
/baseURL:\s*['"]([^'"]+)['"]/,
/baseUrl:\s*['"]([^'"]+)['"]/,
/coverageThreshold:/,
/collectCoverageFrom:/,
/projects:/,
/webServer:/,
/retries:/,
/fullyParallel:/
];
for (const pattern of patterns) {
if (pattern.test(content)) {
// Extract the configuration key
const match = content.match(pattern);
if (match) {
const key = match[0].split(':')[0].trim();
config[key] = true; // Mark as present
}
}
}
return config;
}
catch (error) {
return {};
}
}
/**
* Check if a framework should have a configuration file
*/
async shouldHaveConfig(framework) {
try {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
switch (framework) {
case 'jest':
return 'jest' in allDeps;
case 'playwright':
return '@playwright/test' in allDeps || 'playwright' in allDeps;
case 'cypress':
return 'cypress' in allDeps;
case 'vitest':
return 'vitest' in allDeps;
default:
return false;
}
}
catch (error) {
return false;
}
}
/**
* Calculate configuration quality score
*/
calculateConfigScore(analysis) {
if (!analysis.exists)
return 0;
if (!analysis.valid)
return 10;
let score = 50; // Base score for existing, valid config
// Deduct points for issues
for (const issue of analysis.issues) {
switch (issue.severity) {
case 'error':
score -= 15;
break;
case 'warning':
score -= 10;
break;
case 'info':
score -= 5;
break;
}
}
// Add points for implemented best practices
const implementedPractices = analysis.bestPractices.filter(bp => bp.implemented).length;
const totalPractices = analysis.bestPractices.length;
if (totalPractices > 0) {
score += (implementedPractices / totalPractices) * 30;
}
// Add points for optimization opportunities (indicates thorough analysis)
if (analysis.optimizations.length === 0) {
score += 20; // Well-optimized config
}
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Generate optimization recommendations
*/
async generateOptimizationReport(analyses) {
const validConfigs = analyses.filter(a => a.valid);
const totalScore = validConfigs.reduce((sum, a) => sum + a.score, 0);
const criticalIssues = analyses.reduce((sum, a) => sum + a.issues.filter(i => i.severity === 'error').length, 0);
// Prioritize optimizations
const allOptimizations = analyses.flatMap(a => a.optimizations.map(opt => ({
...opt,
config: a.file,
framework: a.framework
})));
const prioritizedActions = this.prioritizeOptimizations(allOptimizations);
// Identify best practice gaps
const bestPracticeGaps = this.identifyBestPracticeGaps(analyses);
return {
summary: {
totalConfigs: analyses.length,
validConfigs: validConfigs.length,
averageScore: validConfigs.length > 0 ? Math.round(totalScore / validConfigs.length) : 0,
criticalIssues
},
prioritizedActions,
bestPracticeGaps
};
}
prioritizeOptimizations(optimizations) {
const priorities = { high: 3, medium: 2, low: 1 };
return optimizations
.sort((a, b) => (priorities[b.impact] || 1) - (priorities[a.impact] || 1))
.map(opt => ({
priority: opt.impact,
description: opt.description,
implementation: opt.implementation,
affectedConfigs: [opt.config]
}));
}
identifyBestPracticeGaps(analyses) {
const gaps = new Map();
for (const analysis of analyses) {
for (const practice of analysis.bestPractices) {
if (!practice.implemented) {
const key = `${practice.category}-${practice.description}`;
if (gaps.has(key)) {
gaps.get(key).configs.push(analysis.file);
}
else {
gaps.set(key, {
category: practice.category,
description: practice.description,
recommendation: practice.recommendation || 'Consider implementing this best practice',
configs: [analysis.file]
});
}
}
}
}
return Array.from(gaps.values());
}
}
//# sourceMappingURL=config-analyzer.js.map