UNPKG

vineguard-mcp

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

565 lines (557 loc) 17.9 kB
/** * Dependency Manager for VineGuard * Handles automatic installation and configuration of testing dependencies */ import { spawn } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; export class DependencyManager { projectRoot; packageManager = 'npm'; constructor(projectRoot) { this.projectRoot = projectRoot; this.detectPackageManager(); } /** * Detect which package manager is being used */ async detectPackageManager() { try { if (await this.fileExists(path.join(this.projectRoot, 'pnpm-lock.yaml'))) { this.packageManager = 'pnpm'; } else if (await this.fileExists(path.join(this.projectRoot, 'yarn.lock'))) { this.packageManager = 'yarn'; } else { this.packageManager = 'npm'; } } catch (error) { this.packageManager = 'npm'; // Default fallback } } /** * Get recommended testing dependencies for different frameworks */ static getRecommendedDependencies() { return { react: [ { name: 'jest', devDependency: true, framework: 'jest', configFiles: ['jest.config.js', 'jest.config.ts'] }, { name: '@testing-library/react', devDependency: true, framework: 'jest' }, { name: '@testing-library/jest-dom', devDependency: true, framework: 'jest' }, { name: '@testing-library/user-event', devDependency: true, framework: 'jest' }, { name: '@playwright/test', devDependency: true, framework: 'playwright', configFiles: ['playwright.config.ts'] }, { name: 'cypress', devDependency: true, framework: 'cypress', configFiles: ['cypress.config.js', 'cypress.config.ts'] } ], vue: [ { name: 'jest', devDependency: true, framework: 'jest' }, { name: '@vue/test-utils', devDependency: true, framework: 'jest' }, { name: 'vitest', devDependency: true, framework: 'vitest', configFiles: ['vitest.config.ts', 'vite.config.ts'] }, { name: '@playwright/test', devDependency: true, framework: 'playwright' }, { name: 'cypress', devDependency: true, framework: 'cypress' } ], angular: [ { name: '@angular/testing', devDependency: true, framework: 'jest' }, { name: 'jest-preset-angular', devDependency: true, framework: 'jest' }, { name: '@playwright/test', devDependency: true, framework: 'playwright' }, { name: 'cypress', devDependency: true, framework: 'cypress' } ], next: [ { name: 'jest', devDependency: true, framework: 'jest' }, { name: '@testing-library/react', devDependency: true, framework: 'jest' }, { name: 'jest-environment-jsdom', devDependency: true, framework: 'jest' }, { name: '@playwright/test', devDependency: true, framework: 'playwright' } ], node: [ { name: 'jest', devDependency: true, framework: 'jest' }, { name: 'supertest', devDependency: true, framework: 'jest' }, { name: '@playwright/test', devDependency: true, framework: 'playwright' } ] }; } /** * Analyze current project dependencies */ async analyzeDependencies() { const packageJsonPath = path.join(this.projectRoot, 'package.json'); try { const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); const allDeps = { ...packageJson.dependencies || {}, ...packageJson.devDependencies || {} }; // Detect project type const projectType = this.detectProjectType(packageJson); // Get recommended dependencies for project type const recommendedDeps = DependencyManager.getRecommendedDependencies()[projectType] || []; const installed = {}; const missing = []; for (const dep of recommendedDeps) { if (allDeps[dep.name]) { installed[dep.name] = { version: allDeps[dep.name], isDev: packageJson.devDependencies?.[dep.name] !== undefined }; } else { missing.push(dep.name); } } return { installed, missing, projectType }; } catch (error) { throw new Error(`Failed to analyze dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Install missing testing dependencies */ async installMissingDependencies(dependencies, projectType = 'node') { const result = { success: false, installed: [], failed: [], configurations: [], errors: [] }; const recommendedDeps = DependencyManager.getRecommendedDependencies()[projectType] || []; for (const depName of dependencies) { const depInfo = recommendedDeps.find(dep => dep.name === depName); if (!depInfo) { result.errors.push(`Unknown dependency: ${depName}`); continue; } try { console.log(`Installing ${depName}...`); await this.executeCommand(this.packageManager, [ 'install', depInfo.devDependency ? '--save-dev' : '', depName ].filter(Boolean)); result.installed.push(depName); // Create configuration files if needed if (depInfo.configFiles) { for (const configFile of depInfo.configFiles) { const configPath = path.join(this.projectRoot, configFile); if (!(await this.fileExists(configPath))) { const configContent = this.generateConfig(depInfo.framework, configFile); await fs.writeFile(configPath, configContent); result.configurations.push(configFile); } } } } catch (error) { result.failed.push(depName); result.errors.push(`Failed to install ${depName}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } result.success = result.failed.length === 0; return result; } /** * Validate existing configurations */ async validateConfigurations() { const result = { valid: [], invalid: [], missing: [], recommendations: [] }; const configFiles = [ 'jest.config.js', 'jest.config.ts', 'playwright.config.ts', 'cypress.config.js', 'cypress.config.ts', 'vitest.config.ts' ]; for (const configFile of configFiles) { const configPath = path.join(this.projectRoot, configFile); if (await this.fileExists(configPath)) { try { const content = await fs.readFile(configPath, 'utf8'); const validation = this.validateConfigContent(configFile, content); if (validation.valid) { result.valid.push(configFile); } else { result.invalid.push(configFile); result.recommendations.push(...validation.issues); } } catch (error) { result.invalid.push(configFile); result.recommendations.push(`Failed to read ${configFile}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } else { // Check if the framework is installed but config is missing const framework = this.getFrameworkFromConfigFile(configFile); const { installed } = await this.analyzeDependencies(); if (this.hasFrameworkDependency(framework, installed)) { result.missing.push(configFile); result.recommendations.push(`Create ${configFile} for optimal ${framework} configuration`); } } } return result; } /** * Generate optimal configuration for testing frameworks */ generateConfig(framework, filename) { switch (framework) { case 'jest': if (filename.endsWith('.ts')) { return `import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'], moduleNameMapping: { '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/index.tsx', '!src/reportWebVitals.ts', ], coverageThreshold: { global: { branches: 75, functions: 75, lines: 75, statements: 75, }, }, }; export default config;`; } else { return `/** @type {import('jest').Config} */ module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'], moduleNameMapping: { '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, collectCoverageFrom: [ 'src/**/*.{js,jsx}', '!src/index.js', ], coverageThreshold: { global: { branches: 75, functions: 75, lines: 75, statements: 75, }, }, };`; } case 'playwright': return `import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], webServer: { command: 'npm start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });`; case 'cypress': if (filename.endsWith('.ts')) { return `import { defineConfig } from 'cypress' export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', supportFile: 'cypress/support/e2e.ts', specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', }, component: { devServer: { framework: 'react', bundler: 'vite', }, }, })`; } else { return `const { defineConfig } = require('cypress') module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', supportFile: 'cypress/support/e2e.js', specPattern: 'cypress/e2e/**/*.cy.{js,jsx}', }, })`; } case 'vitest': return `import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['src/setupTests.ts'], globals: true, css: true, coverage: { provider: 'c8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src/setupTests.ts', ], }, }, })`; default: return `// Configuration for ${framework}`; } } /** * Validate configuration file content */ validateConfigContent(filename, content) { const issues = []; let valid = true; // Basic validation - check for common issues if (content.trim().length === 0) { valid = false; issues.push(`${filename} is empty`); } // Framework-specific validation const framework = this.getFrameworkFromConfigFile(filename); switch (framework) { case 'jest': if (!content.includes('testEnvironment')) { issues.push('Consider specifying testEnvironment in Jest config'); } if (!content.includes('coverageThreshold')) { issues.push('Consider adding coverage thresholds for better quality control'); } break; case 'playwright': if (!content.includes('baseURL')) { issues.push('Consider specifying baseURL for consistent testing'); } if (!content.includes('projects')) { issues.push('Consider configuring multiple browser projects'); } break; case 'cypress': if (!content.includes('baseUrl')) { issues.push('Consider specifying baseUrl for Cypress tests'); } break; } return { valid, issues }; } /** * Detect project type from package.json */ detectProjectType(packageJson) { const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (deps.react || deps['@types/react']) return 'react'; if (deps.vue || deps['@vue/core']) return 'vue'; if (deps['@angular/core']) return 'angular'; if (deps.next) return 'next'; if (deps.svelte) return 'svelte'; if (deps.astro) return 'astro'; return 'node'; } /** * Get framework name from config file */ getFrameworkFromConfigFile(filename) { if (filename.includes('jest')) return 'jest'; if (filename.includes('playwright')) return 'playwright'; if (filename.includes('cypress')) return 'cypress'; if (filename.includes('vitest')) return 'vitest'; return 'unknown'; } /** * Check if framework dependency exists */ hasFrameworkDependency(framework, installed) { switch (framework) { case 'jest': return 'jest' in installed; case 'playwright': return '@playwright/test' in installed || 'playwright' in installed; case 'cypress': return 'cypress' in installed; case 'vitest': return 'vitest' in installed; default: return false; } } /** * Check if file exists */ async fileExists(filepath) { try { await fs.access(filepath); return true; } catch { return false; } } /** * Execute shell command */ async executeCommand(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: this.projectRoot, stdio: 'pipe' }); let stderr = ''; child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Command failed with code ${code}: ${stderr}`)); } }); child.on('error', (error) => { reject(error); }); }); } } //# sourceMappingURL=dependency-manager.js.map