@versatil/sdlc-framework
Version:
🚀 AI-Native SDLC framework with 11-MCP ecosystem, RAG memory, OPERA orchestration, and 6 specialized agents achieving ZERO CONTEXT LOSS. Features complete CI/CD pipeline with 7 GitHub workflows (MCP testing, security scanning, performance benchmarking),
315 lines • 13.3 kB
JavaScript
/**
* Shadcn MCP Executor - Production Implementation
* Real component analysis using ts-morph AST parsing
*/
import { Project, SyntaxKind } from 'ts-morph';
import { glob } from 'glob';
import * as path from 'path';
import * as fs from 'fs';
import { DEFAULT_SHADCN_MCP_CONFIG } from './shadcn-mcp-config.js';
export class ShadcnMCPExecutor {
constructor(config = {}) {
this.config = { ...DEFAULT_SHADCN_MCP_CONFIG, ...config };
this.project = new Project({
tsConfigFilePath: path.join(this.config.projectPath, 'tsconfig.json'),
skipAddingFilesFromTsConfig: true
});
}
/**
* Execute Shadcn MCP action
*/
async executeShadcnMCP(action, params = {}) {
const startTime = Date.now();
try {
console.log(`🎨 JAMES-FRONTEND: Executing Shadcn MCP action: ${action}`);
switch (action) {
case 'component_analysis':
return await this.scanProject();
case 'component_usage':
return await this.analyzeComponentUsage(params.componentName);
case 'unused_components':
return await this.findUnusedComponents();
case 'accessibility_check':
return await this.validateAccessibility(params.componentName);
default:
throw new Error(`Unknown Shadcn MCP action: ${action}`);
}
}
catch (error) {
return {
success: false,
error: error.message,
executionTime: Date.now() - startTime
};
}
}
/**
* Scan project for Shadcn components
*/
async scanProject() {
const startTime = Date.now();
try {
console.log(`🔍 Scanning project for Shadcn components...`);
// Find all installed components
const componentsDir = path.join(this.config.projectPath, this.config.componentsPath);
const installed = this.findInstalledComponents(componentsDir);
// Find all source files
const sourceFiles = await glob(this.config.filePatterns, {
cwd: this.config.projectPath,
ignore: this.config.excludePatterns,
absolute: true
});
// Add files to project
this.project.addSourceFilesAtPaths(sourceFiles);
// Analyze component usage
const usageMap = this.analyzeUsage(installed);
// Calculate statistics
const used = Array.from(usageMap.entries()).map(([component, usage]) => ({
component,
usageCount: usage.files.size,
files: Array.from(usage.files),
variants: Array.from(usage.variants)
}));
const usedNames = new Set(used.map(u => u.component));
const unused = installed.filter(c => !usedNames.has(c.name)).map(c => c.name);
const result = {
installed,
used,
unused,
statistics: {
totalComponents: installed.length,
utilizationRate: Math.round((used.length / installed.length) * 100),
mostUsed: used.sort((a, b) => b.usageCount - a.usageCount)[0]?.component || 'None'
}
};
console.log(`✅ Scan complete: ${installed.length} components, ${used.length} used, ${unused.length} unused`);
return {
success: true,
data: result,
executionTime: Date.now() - startTime
};
}
catch (error) {
return {
success: false,
error: `Scan failed: ${error.message}`,
executionTime: Date.now() - startTime
};
}
}
/**
* Analyze specific component usage
*/
async analyzeComponentUsage(componentName) {
const startTime = Date.now();
try {
console.log(`📊 Analyzing usage of ${componentName}...`);
const usage = [];
const propsMap = {};
for (const sourceFile of this.project.getSourceFiles()) {
const imports = sourceFile.getImportDeclarations();
for (const importDecl of imports) {
const importPath = importDecl.getModuleSpecifierValue();
if (importPath.includes(componentName.toLowerCase()) ||
importPath.includes('@/components/ui')) {
const namedImports = importDecl.getNamedImports();
for (const namedImport of namedImports) {
if (namedImport.getName() === componentName) {
// Find JSX elements using this component
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
for (const jsxElement of jsxElements) {
const tagName = jsxElement.getTagNameNode().getText();
if (tagName === componentName) {
const attributes = jsxElement.getAttributes();
const props = {};
for (const attr of attributes) {
if (attr.getKind() === SyntaxKind.JsxAttribute) {
const jsxAttr = attr.asKind(SyntaxKind.JsxAttribute);
if (jsxAttr) {
const propName = jsxAttr.getNameNode().getText();
props[propName] = jsxAttr.getInitializer()?.getText() || 'true';
propsMap[propName] = (propsMap[propName] || 0) + 1;
}
}
}
usage.push({
file: sourceFile.getFilePath(),
line: jsxElement.getStartLineNumber(),
props
});
}
}
}
}
}
}
}
const report = {
component: componentName,
usage,
patterns: {
commonProps: propsMap,
customizationLevel: Object.keys(propsMap).length > 5 ? 'high' :
Object.keys(propsMap).length > 2 ? 'medium' : 'low'
},
recommendations: this.generateComponentRecommendations(componentName, usage, propsMap)
};
console.log(`✅ Analysis complete: ${usage.length} usages found`);
return {
success: true,
data: report,
executionTime: Date.now() - startTime
};
}
catch (error) {
return {
success: false,
error: `Usage analysis failed: ${error.message}`,
executionTime: Date.now() - startTime
};
}
}
/**
* Find unused components
*/
async findUnusedComponents() {
const startTime = Date.now();
try {
const scanResult = await this.scanProject();
if (!scanResult.success) {
throw new Error('Failed to scan project');
}
const unused = scanResult.data.unused;
console.log(`✅ Found ${unused.length} unused components`);
return {
success: true,
data: { unused, count: unused.length },
executionTime: Date.now() - startTime
};
}
catch (error) {
return {
success: false,
error: `Unused components check failed: ${error.message}`,
executionTime: Date.now() - startTime
};
}
}
/**
* Validate accessibility (simplified check)
*/
async validateAccessibility(componentName) {
const startTime = Date.now();
try {
console.log(`♿ Validating accessibility for ${componentName}...`);
const issues = [];
for (const sourceFile of this.project.getSourceFiles()) {
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
for (const jsxElement of jsxElements) {
if (jsxElement.getTagNameNode().getText() === componentName) {
const hasAriaLabel = jsxElement.getAttributes().some(attr => {
if (attr.getKind() === SyntaxKind.JsxAttribute) {
const jsxAttr = attr.asKind(SyntaxKind.JsxAttribute);
const attrName = jsxAttr?.getNameNode().getText();
return attrName === 'aria-label' || attrName === 'aria-labelledby';
}
return false;
});
if (!hasAriaLabel && componentName.toLowerCase().includes('button')) {
issues.push({
type: 'missing-aria',
severity: 'warning',
file: sourceFile.getFilePath(),
line: jsxElement.getStartLineNumber(),
description: `${componentName} missing aria-label`,
suggestion: 'Add aria-label or aria-labelledby attribute'
});
}
}
}
}
console.log(`✅ Accessibility check complete: ${issues.length} issues found`);
return {
success: true,
data: { issues, count: issues.length },
executionTime: Date.now() - startTime
};
}
catch (error) {
return {
success: false,
error: `Accessibility check failed: ${error.message}`,
executionTime: Date.now() - startTime
};
}
}
/**
* Helper: Find installed Shadcn components
*/
findInstalledComponents(componentsDir) {
if (!fs.existsSync(componentsDir)) {
return [];
}
const files = fs.readdirSync(componentsDir);
return files
.filter(file => file.endsWith('.tsx') || file.endsWith('.ts'))
.map(file => ({
name: this.pascalCase(path.basename(file, path.extname(file))),
path: path.join(componentsDir, file)
}));
}
/**
* Helper: Analyze component usage across project
*/
analyzeUsage(installed) {
const usageMap = new Map();
for (const { name } of installed) {
const usage = { files: new Set(), variants: new Set() };
for (const sourceFile of this.project.getSourceFiles()) {
const imports = sourceFile.getImportDeclarations();
for (const importDecl of imports) {
const namedImports = importDecl.getNamedImports();
if (namedImports.some(ni => ni.getName() === name)) {
usage.files.add(sourceFile.getFilePath());
}
}
}
if (usage.files.size > 0) {
usageMap.set(name, usage);
}
}
return usageMap;
}
/**
* Helper: Generate component recommendations
*/
generateComponentRecommendations(componentName, usage, propsMap) {
const recommendations = [];
if (usage.length === 0) {
recommendations.push(`${componentName} is not being used - consider removing if unnecessary`);
}
else if (usage.length > 50) {
recommendations.push(`${componentName} is heavily used - ensure it's well-tested`);
}
const commonProps = Object.entries(propsMap)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([prop]) => prop);
if (commonProps.length > 0) {
recommendations.push(`Most common props: ${commonProps.join(', ')}`);
}
return recommendations.length > 0 ? recommendations : ['Component usage looks healthy'];
}
/**
* Helper: Convert to PascalCase
*/
pascalCase(str) {
return str
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}
}
// Export singleton instance
export const shadcnMCPExecutor = new ShadcnMCPExecutor();
//# sourceMappingURL=shadcn-mcp-executor.js.map