lwc-generator-cli
Version:
AI-powered Lightning Web Component generator with interactive web UI
363 lines (319 loc) ⢠12.3 kB
JavaScript
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
// AI Provider configurations
const AI_PROVIDERS = {
ollama: {
endpoint: (url) => `${url}/api/generate`,
defaultModel: 'llama3.2',
formatRequest: (prompt, model) => ({
model,
prompt,
stream: false
}),
extractResponse: (data) => data.response
},
openai: {
endpoint: () => 'https://api.openai.com/v1/chat/completions',
defaultModel: 'gpt-4',
formatRequest: (prompt, model, apiKey) => ({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 4096
}),
extractResponse: (data) => data.choices[0].message.content
},
anthropic: {
endpoint: () => 'https://api.anthropic.com/v1/messages',
defaultModel: 'claude-sonnet-4.5-20250929',
formatRequest: (prompt, model, apiKey) => ({
model,
max_tokens: 8000,
messages: [{ role: 'user', content: prompt }]
}),
extractResponse: (data) => data.content[0].text
},
groq: {
endpoint: () => 'https://api.groq.com/openai/v1/chat/completions',
defaultModel: 'mixtral-8x7b-32768',
formatRequest: (prompt, model, apiKey) => ({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 8000
}),
headers: (apiKey) => ({
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}),
extractResponse: (data) => data.choices[0].message.content
},
grok: {
endpoint: () => 'https://api.x.ai/v1/chat/completions',
defaultModel: 'grok-beta',
formatRequest: (prompt, model, apiKey) => ({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 8000
}),
extractResponse: (data) => data.choices[0].message.content
}
};
async function callAI(provider, prompt, apiKey, modelName, ollamaUrl) {
const config = AI_PROVIDERS[provider];
if (!config) {
throw new Error(`Unsupported AI provider: ${provider}`);
}
const model = modelName || config.defaultModel;
const endpoint = provider === 'ollama' ? config.endpoint(ollamaUrl) : config.endpoint();
const requestData = config.formatRequest(prompt, model, apiKey);
try {
let axiosConfig = {};
// Handle headers separately for different providers
if (provider === 'ollama') {
axiosConfig.headers = { 'Content-Type': 'application/json' };
} else if (provider === 'openai' || provider === 'groq') {
axiosConfig.headers = {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
};
} else if (provider === 'anthropic') {
axiosConfig.headers = {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json'
};
}
// Add timeout
axiosConfig.timeout = 120000; // 2 minutes
const response = await axios.post(endpoint, requestData, axiosConfig);
return config.extractResponse(response.data);
} catch (error) {
if (error.response) {
// Log more details for debugging
console.error('API Error Response:', {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data
});
throw new Error(`AI API call failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
} else if (error.request) {
throw new Error(`AI API call failed: No response received from ${provider}`);
} else {
throw new Error(`AI API call failed: ${error.message}`);
}
}
}
function createLWCPrompt(requirement) {
return `You are an expert Salesforce Lightning Web Component developer. Generate a complete LWC component based on the following requirements.
REQUIREMENTS:
${requirement}
Generate the following files with complete, production-ready code:
1. **JavaScript Controller** (componentName.js)
2. **HTML Template** (componentName.html)
3. **CSS Stylesheet** (componentName.css)
4. **Metadata XML** (componentName.js-meta.xml)
5. **Apex Controller** (if server-side logic is needed)
6. **Apex Test Class** (for the Apex controller)
IMPORTANT INSTRUCTIONS:
- Choose an appropriate component name based on requirements (camelCase)
- Follow Salesforce LWC best practices
- Use modern JavaScript (ES6+)
- Include proper error handling
- Add meaningful comments
- Use Lightning Design System (SLDS) classes
- Implement proper security (with sharing in Apex)
- Ensure accessibility (ARIA labels)
- Make it production-ready
OUTPUT FORMAT - CRITICAL:
Respond with ONLY a valid JSON object. Do NOT use markdown code blocks.
Do NOT use backticks (\`) for string values - use double quotes (") only.
All file content must be properly escaped JSON strings.
Use this EXACT structure:
{
"componentName": "componentName",
"files": [
{
"name": "componentName.js",
"type": "javascript",
"content": "import { LightningElement } from 'lwc';\\n\\nexport default class ComponentName extends LightningElement {\\n // Your code here\\n}"
},
{
"name": "componentName.html",
"type": "html",
"content": "<template>\\n <!-- Your HTML here -->\\n</template>"
},
{
"name": "componentName.css",
"type": "css",
"content": "/* Your CSS here */"
},
{
"name": "componentName.js-meta.xml",
"type": "xml",
"content": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<LightningComponentBundle xmlns=\\"http://soap.sforce.com/2006/04/metadata\\">\\n</LightningComponentBundle>"
},
{
"name": "ComponentNameController.cls",
"type": "apex",
"content": "public with sharing class ComponentNameController {\\n // Your Apex code\\n}"
},
{
"name": "ComponentNameController.cls-meta.xml",
"type": "xml",
"content": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<ApexClass xmlns=\\"http://soap.sforce.com/2006/04/metadata\\">\\n <apiVersion>60.0</apiVersion>\\n <status>Active</status>\\n</ApexClass>"
},
{
"name": "ComponentNameControllerTest.cls",
"type": "apex",
"content": "@isTest\\nprivate class ComponentNameControllerTest {\\n // Test methods\\n}"
},
{
"name": "ComponentNameControllerTest.cls-meta.xml",
"type": "xml",
"content": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<ApexClass xmlns=\\"http://soap.sforce.com/2006/04/metadata\\">\\n <apiVersion>60.0</apiVersion>\\n <status>Active</status>\\n</ApexClass>"
}
]
}
CRITICAL RULES FOR JSON:
1. Use double quotes (") for all strings, not backticks (\`)
2. Escape special characters: \\n for newlines, \\" for quotes, \\\\ for backslashes
3. Do not wrap the JSON in markdown code blocks
4. Ensure the JSON is valid and parseable
5. All content fields must be valid JSON strings
RESPOND ONLY WITH THE JSON OBJECT. NO EXPLANATORY TEXT BEFORE OR AFTER.`;
}
function extractJSON(text) {
// Remove markdown code blocks if present
let cleaned = text.replace(/```json\n?/g, '').replace(/```\n?/g, '');
// Try to find JSON object in the response
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in AI response');
}
let jsonStr = jsonMatch[0];
// Handle common issues with AI-generated JSON
// Replace backticks with escaped quotes in content fields
jsonStr = jsonStr.replace(/("content":\s*)`([^`]*)`/g, (match, prefix, content) => {
// Escape quotes and newlines in the content
const escaped = content
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
return `${prefix}"${escaped}"`;
});
// Try parsing with multiple strategies
try {
return JSON.parse(jsonStr);
} catch (error) {
// Strategy 2: Try to fix common issues
try {
// Remove trailing commas
jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1');
return JSON.parse(jsonStr);
} catch (error2) {
// Strategy 3: Use a more lenient parser
try {
// Replace all backtick-quoted strings with proper JSON strings
jsonStr = jsonStr.replace(/`([^`]*)`/g, (match, content) => {
return JSON.stringify(content);
});
return JSON.parse(jsonStr);
} catch (error3) {
// Log the problematic JSON for debugging
console.error('\nā Failed to parse JSON. First 500 chars of response:');
console.error(jsonStr.substring(0, 500));
console.error('\nš” Tip: The AI response format might be incorrect. Try a different model or regenerate.');
throw new Error(`Failed to parse JSON after multiple attempts: ${error.message}`);
}
}
}
}
async function generateLWC(options) {
const {
requirement,
model,
apiKey,
modelName,
ollamaUrl,
outputDir
} = options;
// Create the prompt
const prompt = createLWCPrompt(requirement);
// Call AI to generate component with retry logic
let aiResponse;
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
try {
attempts++;
console.log(`\nš Attempt ${attempts}/${maxAttempts}...`);
aiResponse = await callAI(model, prompt, apiKey, modelName, ollamaUrl);
// Try to extract and parse JSON
const result = extractJSON(aiResponse);
// Validate result structure
if (!result.componentName || !result.files || !Array.isArray(result.files)) {
throw new Error('Invalid response structure from AI');
}
if (result.files.length === 0) {
throw new Error('No files generated by AI');
}
// Success! Continue with file operations
console.log(`ā
Successfully parsed response with ${result.files.length} files`);
// Create Salesforce DX project structure
const projectRoot = path.join(outputDir, result.componentName);
const lwcDir = path.join(projectRoot, 'force-app', 'main', 'default', 'lwc', result.componentName);
const classesDir = path.join(projectRoot, 'force-app', 'main', 'default', 'classes');
// Ensure directories exist
await fs.mkdir(lwcDir, { recursive: true });
await fs.mkdir(classesDir, { recursive: true });
// Write files and calculate sizes
const filesWithMetadata = [];
for (const file of result.files) {
const isApex = file.type === 'apex' || file.name.endsWith('.cls') ||
(file.name.endsWith('.cls-meta.xml') && !file.name.includes(result.componentName + '.js-meta.xml'));
const targetDir = isApex ? classesDir : lwcDir;
const filePath = path.join(targetDir, file.name);
await fs.writeFile(filePath, file.content, 'utf-8');
const stats = await fs.stat(filePath);
// Calculate relative path for display
let relativePath;
if (isApex) {
relativePath = `force-app/main/default/classes/${file.name}`;
} else {
relativePath = `force-app/main/default/lwc/${result.componentName}/${file.name}`;
}
filesWithMetadata.push({
...file,
size: stats.size,
path: filePath,
relativePath: relativePath
});
}
return {
componentName: result.componentName,
files: filesWithMetadata,
outputDir: projectRoot
};
} catch (error) {
console.error(`ā Attempt ${attempts} failed: ${error.message}`);
if (attempts >= maxAttempts) {
console.error('\nš” Troubleshooting suggestions:');
console.error(' 1. Try a different AI model (e.g., --model-name codellama)');
console.error(' 2. Simplify your requirement');
console.error(' 3. Try a different AI provider (e.g., -m anthropic)');
console.error(' 4. Check if Ollama is running: curl http://localhost:11434/api/tags');
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
module.exports = {
generateLWC
};