UNPKG

aem-component-generator

Version:

AI-powered AEM component generator that creates React components, Sling models, dialogs, and comprehensive test suites using Google Gemini AI

203 lines (168 loc) โ€ข 8.68 kB
#!/usr/bin/env node /** * AEM Component Generator * AI-powered tool for generating complete AEM components with tests * * @author AEM Component Generator * @version 1.0.0 * @license MIT */ import { Command } from 'commander'; import { GoogleGenerativeAI } from '@google/generative-ai'; import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import { fileURLToPath } from 'url'; // Get current file path for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Temporarily disable SSL verification if needed (corporate networks, etc.) if (!process.env.NODE_TLS_REJECT_UNAUTHORIZED) { process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; } const program = new Command(); // Check for API key if (!process.env.GEMINI_API_KEY) { console.error('Error: GEMINI_API_KEY environment variable is not set.'); console.error('Please set your Gemini API key:'); console.error(' PowerShell: $env:GEMINI_API_KEY="your-api-key-here"'); console.error(' Command Prompt: set GEMINI_API_KEY=your-api-key-here'); console.error('\nGet your API key from: https://makersuite.google.com/app/apikey'); process.exit(1); } const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); handlebars.registerHelper('ifEquals', function(arg1, arg2, options) { return (arg1 == arg2) ? options.fn(this) : options.inverse(this); }); handlebars.registerHelper('capitalize', function(str) { return str.charAt(0).toUpperCase() + str.slice(1); }); program .name('aem-gen') .description('AI-powered AEM component generator using Google Gemini AI') .version('1.0.0') .argument('<prompt...>', 'component description prompt') .option('-o, --output <directory>', 'output directory (default: current directory)', process.cwd()) .option('-p, --package <package>', 'Java package name', 'com.myproject.core.models') .option('-v, --verbose', 'verbose output') .action(async (promptWords, options) => { try { const prompt = promptWords.join(' '); const outDir = options.output; const templatesDir = path.join(__dirname, 'templates'); if (options.verbose) { console.log(`๐Ÿ” Input: "${prompt}"`); console.log(`๐Ÿ“ Output Directory: ${outDir}`); console.log(`๐Ÿ“ฆ Package: ${options.package}`); console.log(`๐Ÿ“‹ Templates Directory: ${templatesDir}`); } console.log(`Generating component for: "${prompt}"`); const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); const result = await model.generateContent(` Generate a component scaffold with fields parsed from: "${prompt}" Return JSON like: { name: "heroBanner", className: "HeroBanner", package: "${options.package}", fields: [ { name: "title", type: "String", label: "Title" }, { name: "subtitle", type: "String", label: "Subtitle" }, { name: "backgroundImage", type: "String", label: "Background Image" }, { name: "ctaText", type: "String", label: "CTA Text" }, { name: "ctaLink", type: "String", label: "CTA Link" } ] }`); const responseText = result.response.text(); if (options.verbose) { console.log('๐Ÿค– Raw AI response:', responseText); } // Clean the response - remove markdown code blocks if present let cleanedResponse = responseText.trim(); if (cleanedResponse.startsWith('```json')) { cleanedResponse = cleanedResponse.replace(/^```json\s*/, '').replace(/\s*```$/, ''); } else if (cleanedResponse.startsWith('```')) { cleanedResponse = cleanedResponse.replace(/^```\s*/, '').replace(/\s*```$/, ''); } // Remove JavaScript-style comments that make JSON invalid cleanedResponse = cleanedResponse.replace(/\/\/.*$/gm, ''); const modelData = JSON.parse(cleanedResponse); // Create proper AEM component folder structure const componentName = modelData.name; const className = modelData.className; // Define output paths for AEM component structure including tests const outputPaths = { 'component.scss.hbs': `ui.frontend/src/main/webpack/components/${componentName}/${className}.scss`, 'component.tsx.hbs': `ui.frontend/src/main/webpack/components/${componentName}/${className}.tsx`, 'dialog.xml.hbs': `ui.apps/src/main/content/jcr_root/apps/myproject/components/${componentName}/_cq_dialog/.content.xml`, 'slingModel.java.hbs': `core/src/main/java/${modelData.package.replace(/\./g, '/')}/${className}.java`, 'wrapper.html.hbs': `ui.apps/src/main/content/jcr_root/apps/myproject/components/${componentName}/${componentName}.html`, // Frontend tests 'component.test.tsx.hbs': `ui.frontend/src/test/components/${componentName}/${className}.test.tsx`, 'component.stories.tsx.hbs': `ui.frontend/src/test/components/${componentName}/${className}.stories.tsx`, // Backend tests 'slingModel.test.java.hbs': `core/src/test/java/${modelData.package.replace(/\./g, '/')}/${className}Test.java`, 'component.integration.test.java.hbs': `it.tests/src/main/java/${modelData.package.replace(/\./g, '/')}/it/${className}IT.java` }; const tplFiles = fs.readdirSync(templatesDir); console.log(`\nCreating component files for: ${className}`); console.log('=====================================\n'); tplFiles.forEach(file => { const tpl = handlebars.compile(fs.readFileSync(path.join(templatesDir, file), 'utf8')); const content = tpl({ model: modelData }); const outputPath = outputPaths[file]; if (outputPath) { const fullOutputPath = path.join(outDir, outputPath); const outputDir = path.dirname(fullOutputPath); // Create directory if it doesn't exist fs.mkdirSync(outputDir, { recursive: true }); // Write the file fs.writeFileSync(fullOutputPath, content, 'utf8'); console.log(`โœ… Created: ${outputPath}`); } else { console.log(`โš ๏ธ Unknown template: ${file}`); } }); console.log(`\n๐ŸŽ‰ Component '${className}' generated successfully!`); console.log('\n๐Ÿ“ฆ Generated Component Files:'); console.log(` ๐Ÿ“ ${outputPaths['component.scss.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['component.tsx.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['dialog.xml.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['slingModel.java.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['wrapper.html.hbs']}`); console.log('\n๐Ÿงช Generated Test Files:'); console.log(' Frontend Tests:'); console.log(` ๐Ÿ“ ${outputPaths['component.test.tsx.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['component.stories.tsx.hbs']}`); console.log(' Backend Tests:'); console.log(` ๐Ÿ“ ${outputPaths['slingModel.test.java.hbs']}`); console.log(` ๐Ÿ“ ${outputPaths['component.integration.test.java.hbs']}`); console.log('\n๐Ÿš€ Next Steps:'); console.log(' 1. Run frontend tests: npm test'); console.log(' 2. Run Storybook: npm run storybook'); console.log(' 3. Run backend tests: mvn test'); console.log(' 4. Run integration tests: mvn verify -Pintegration-tests'); } catch (error) { console.error('Error occurred:'); if (error.message.includes('fetch failed')) { console.error('Network error: Failed to connect to Gemini API'); console.error('Please check:'); console.error('1. Your internet connection'); console.error('2. Your API key is valid'); console.error('3. You have API quota remaining'); } else if (error.message.includes('overloaded')) { console.error('โณ Gemini API is temporarily overloaded'); console.error('Please wait a moment and try again'); console.error('๐Ÿ’ก Tip: Try using a different model like gemini-1.5-pro if this persists'); } else if (error.status === 503) { console.error('๐Ÿ”ด Service temporarily unavailable (503)'); console.error('The Gemini API service is currently down. Please try again later.'); } else if (error instanceof SyntaxError) { console.error('Failed to parse AI response as JSON'); console.error('Raw response might not be valid JSON format'); } else { console.error('Unexpected error:', error.message); } console.error('\nFull error details:', error); process.exit(1); } }); program.parse();