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
JavaScript
/**
* 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