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
JavaScript
/**
* 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();