UNPKG

lwc-generator-cli

Version:

AI-powered Lightning Web Component generator with interactive web UI

363 lines (319 loc) • 12.3 kB
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 };