skaya
Version:
CLI SDK for full-stack automation: scaffold frontend, backend & blockchain. Future-ready for Web3, integrations, server components & logging.
341 lines (320 loc) • 17.3 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateCodeWithAI = generateCodeWithAI;
const generative_ai_1 = require("@google/generative-ai");
const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises")); // Import fs/promises for async file operations
const enums_1 = require("../../bin/types/enums");
const config_1 = require("../config");
// Directory where framer-motion llm files are downloaded
const DOWNLOAD_DIR = path_1.default.join(__dirname, './llm/motion');
/**
* Reads all markdown files from a given directory and concatenates their content.
* @param directoryPath The path to the directory containing markdown files.
* @returns A promise that resolves with the concatenated content of all markdown files.
*/
function readAllMarkdownDocs(directoryPath) {
return __awaiter(this, void 0, void 0, function* () {
let allContent = '';
try {
const files = yield promises_1.default.readdir(directoryPath, { recursive: true });
for (const file of files) {
const fullPath = path_1.default.join(directoryPath, file);
const stats = yield promises_1.default.stat(fullPath);
if (stats.isFile() && file.endsWith('.md')) {
try {
const content = yield promises_1.default.readFile(fullPath, 'utf8');
// Add a separator and filename for clarity in the AI prompt
allContent += `\n--- START DOCUMENT: ${file} ---\n`;
allContent += content;
allContent += `\n--- END DOCUMENT: ${file} ---\n`;
}
catch (error) {
console.error(`Error reading ${fullPath}:`, error);
}
}
}
}
catch (error) {
console.error(`Error accessing directory ${directoryPath}:`, error);
return ""; // Return empty string if directory doesn't exist or is unreadable
}
return allContent;
});
}
function generateCodeWithAI(fileName_1, projectType_1, componentType_1) {
return __awaiter(this, arguments, void 0, function* (fileName, projectType, componentType, aiDescription = "", options = {
style: "css",
typescript: true,
withProps: true,
withState: false,
withEffects: false,
withTests: true,
withStories: projectType === enums_1.ProjectType.FRONTEND,
}, templateFiles = [], updateExistingTemplateFiles, extraOptions = {}) {
const apiKey = (0, config_1.getApiKey)();
if (!apiKey)
throw new Error(`API key is required.`);
const genAI = new generative_ai_1.GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const updatedFiles = [];
const baseConfig = Object.assign({ componentName: fileName, aiDescription,
projectType,
componentType }, options);
// --- NEW: Read all markdown documentation for AI context ---
const allDocsContent = yield readAllMarkdownDocs(DOWNLOAD_DIR);
// Sort template files to ensure main component is processed first
const sortedTemplateFiles = [...templateFiles].sort((a, b) => {
// Main component should come first
if (a.targetFileName === `${fileName}.tsx`)
return -1;
if (b.targetFileName === `${fileName}.tsx`)
return 1;
// Then process CSS
if (a.targetFileName.endsWith(".css"))
return -1;
if (b.targetFileName.endsWith(".css"))
return 1;
// Then tests
if (a.targetFileName.includes(".test."))
return -1;
if (b.targetFileName.includes(".test."))
return 1;
// Finally stories
return 0;
});
// Find and generate the main component file first
const componentFile = sortedTemplateFiles.find((file) => file.targetFileName === `${fileName}.tsx` ||
file.targetFileName === `${fileName}.jsx`);
if (!componentFile) {
throw new Error(`No component file found for ${fileName}`);
}
// Generate the component first
const componentExt = path_1.default
.extname(componentFile.originalFileName)
.replace(".", "");
const { systemPrompt: componentSystemPrompt, userPrompt: componentUserPrompt, } = yield getFileSpecificPrompts(componentExt, componentType, componentFile.content || "", baseConfig, componentFile.originalFileName, componentFile.targetFileName, extraOptions.componentsToImport, undefined, updateExistingTemplateFiles, allDocsContent // --- NEW: Pass allDocsContent here ---
);
const componentContent = yield generateWithGemini(model, componentSystemPrompt, componentUserPrompt);
const generatedComponentContent = componentContent;
updatedFiles.push(Object.assign(Object.assign({}, componentFile), { content: generatedComponentContent }));
// Now generate supporting files in order
for (const fileTemplate of sortedTemplateFiles.filter((file) => file !== componentFile)) {
const fileNameParts = fileTemplate.originalFileName.split(".");
let fileType = fileNameParts.length > 1 ? fileNameParts.slice(1).join(".") : "";
const { systemPrompt, userPrompt } = yield getFileSpecificPrompts(fileType, componentType, fileTemplate.content || "", baseConfig, fileTemplate.originalFileName, fileTemplate.targetFileName, extraOptions.componentsToImport, generatedComponentContent, // Always use the newly generated component
updateExistingTemplateFiles, allDocsContent // --- NEW: Pass allDocsContent here ---
);
const aiUpdatedContent = yield generateWithGemini(model, systemPrompt, userPrompt);
updatedFiles.push(Object.assign(Object.assign({}, fileTemplate), { content: aiUpdatedContent }));
}
return updatedFiles;
});
}
function getFileSpecificPrompts(fileType_1, componentType_1, originalContent_1, baseConfig_1, originalFileName_1, targetFileName_1, componentsToImport_1, componentContent_1, updateExistingTemplateFiles_1) {
return __awaiter(this, arguments, void 0, function* (fileType, componentType, originalContent, baseConfig, originalFileName, targetFileName, componentsToImport, componentContent, updateExistingTemplateFiles, allDocsContent = "" // --- NEW: Add this parameter with a default empty string ---
) {
var _a, _b, _c;
// Base system prompt
const baseSystemPrompt = `You are an expert full-stack developer. Generate clean, production-ready code that:
- Follows best practices for the specific file type
- Is fully functional
- Matches modern architectural patterns
- Maintains consistent theming with other related files
- Uses the correct target file name (${targetFileName}) for all references.
- if ${updateExistingTemplateFiles} 'Update the existing component template files '
- `;
// Extract base component name without extension
const baseFileName = targetFileName.replace(/\.tsx$/, "");
const isTestFile = originalFileName.includes(".test.");
const isStoriesFile = originalFileName.includes(".stories.");
// Create imports string if componentsToImport exists
let importsPrompt = "";
let componentsUsageExample = "";
if (componentsToImport && componentsToImport.length > 0) {
importsPrompt = `\nComponents to import and use:\n`;
componentsUsageExample = `\n\nComponent Usage Requirements:\n`;
componentsToImport.forEach((comp) => {
importsPrompt += `- ${comp.name}: ${comp.data}\n`;
// Extract prop types from the component data if available
const propsMatch = comp.data.match(/interface\s+(\w+)Props\s*{([^}]*)}/);
if (propsMatch) {
const interfaceName = propsMatch[1];
const propsContent = propsMatch[2];
componentsUsageExample += `- For ${comp.name}, use these props (${interfaceName}Props): ${propsContent}\n`;
}
else {
componentsUsageExample += `- For ${comp.name}, use according to its documentation\n`;
}
});
}
// File-specific prompts
if (isTestFile) {
return {
systemPrompt: `${baseSystemPrompt}
- For React component tests only
- Use Testing Library best practices
- Include meaningful test cases that match the component's theme
- Test all component functionality
- Keep the same testing approach as shown in the original file
- Import the component using the target file name (${targetFileName})
- Test all imported components' integration`,
userPrompt: `Generate a test file for the following ${targetFileName} component while maintaining consistent theming:
Component Description: ${baseConfig.aiDescription}
Component Type: ${baseConfig.componentType}
Target File Name: ${targetFileName}
${importsPrompt}
Component Content to test:
${componentContent}
Original test: ${originalContent}
Key Requirements:
1. Import the component from '${targetFileName}'
2. Create comprehensive tests that cover all functionality
3. Test all props and interactions
4. Use Testing Library best practices
5. Include accessibility tests if applicable
6. Implement the described functionality: ${baseConfig.aiDescription} and if ${updateExistingTemplateFiles} Update only the code Make sure no much modigfication is required in the code
7. Test integration with imported components: ${componentsUsageExample}
8. make sure to import ${targetFileName}.css
Return ONLY the test file content with no additional explanations.`,
};
}
else if (isStoriesFile) {
return {
systemPrompt: `${baseSystemPrompt}
- For Storybook stories only
- Use StoryObj type for stories
- Include proper Meta configuration
- Add comprehensive controls that match the component's theme
- Maintain the same story structure as the original
- Import the component using the target file name (${targetFileName})
- Include controls for all imported components`,
userPrompt: `Generate a Storybook story file for the following ${targetFileName} component with Component Content:
${componentContent}:
Original Stories: ${originalContent}
${importsPrompt}
Component Description: ${baseConfig.aiDescription}
Component Type: ${baseConfig.componentType}
Target File Name: ${targetFileName}
${componentsUsageExample}
Key Requirements:
1. Import the component from '${targetFileName}'
2. Create a default story with all controls and props from ${componentContent}
3. Add relevant stories that showcase different states
4. Include proper JSDoc documentation
5. Use TypeScript types for all args
6. Implement the described functionality: ${baseConfig.aiDescription} and if ${updateExistingTemplateFiles} Update only the code Make sure no much modigfication is required in the code
7. Include controls and examples for imported components
8. make sure to import ${targetFileName}.css
Return ONLY the story file content with no additional explanations.`,
};
}
else if (fileType === "tsx") {
return {
systemPrompt: `${baseSystemPrompt}
- For React components only
- Original Content: ${originalContent} .
- Include proper TypeScript types and props. take help from mui props but dont import anything from mui.
- Use clean TSX syntax
- if ${updateExistingTemplateFiles} Do not completely change the code, only update it according to ${baseConfig.aiDescription}
- Follow accessibility best practices
- Match the style of the original component
- Use ${targetFileName} as the component name
- Properly integrate all imported components with their required props`,
userPrompt: `Create a React component file based on the following requirements:
Component Name: ${targetFileName}
Component Type: ${baseConfig.componentType}
Style Type: Provide css className with ${baseFileName}.css . later I will add css file. Use framer-motion library for animations.
Target File Name: ${targetFileName}
${importsPrompt}
Import component as : ${componentsUsageExample}
Alwys import components as :
import ${componentsToImport && ((_a = componentsToImport[0]) === null || _a === void 0 ? void 0 : _a.name)} from '@/components/${componentsToImport && ((_b = componentsToImport[0]) === null || _b === void 0 ? void 0 : _b.name)}/${componentsToImport && ((_c = componentsToImport[0]) === null || _c === void 0 ? void 0 : _c.name)}';
Key Requirements:
1. Use TypeScript with proper typing
2. Follow React best practices
3. Include all necessary props
4. Implement the described functionality: ${baseConfig.aiDescription} and if ${updateExistingTemplateFiles} Update only the code Make sure no much modigfication is required in the code
5. Properly integrate all imported components with their required props
6. Provide css className later I will add css file make sure to import ${baseFileName}.css
7.USE MOTION LIBRARY DOCUMENTATION CONTEXT ALWAYS FOR WRITING COMPONENTS: ${allDocsContent}
Return ONLY the component code with no additional explanations.`,
};
}
else if (fileType === "css" || fileType === "scss") {
return {
systemPrompt: `${baseSystemPrompt}
- For CSS/SCSS styles only
- Use modern styling approaches
- Include responsive design
- Follow BEM naming if appropriate
- Match the existing styling patterns
- Update selectors to match ${targetFileName}
- Include styles for any imported components if needed
- Leverage the provided documentation context for deeper understanding of the Motion library and its components to inform styling decisions.`, // Enhanced system prompt
userPrompt: `Generate a style file for the following ${targetFileName} component from Component Content:
${componentContent}:
Original css: ${originalContent}
Component Description: ${baseConfig.aiDescription}
Component Type: ${baseConfig.componentType}
Style Type: ${baseConfig.style}
Target File Name: ${targetFileName}
${importsPrompt}
Key Requirements:
1. Create styles that match the component structure provided in Component Content.
2. Use ${baseConfig.style} syntax.
3. Include responsive design considerations.
4. Follow BEM naming convention if appropriate.
5. Style all interactive states (e.g., hover, active) if applicable.
6. Include styles for imported components if needed.
7. Crucially, interpret the provided Motion Library documentation context to generate highly relevant and effective CSS. For example, if the docs mention 'gestures' or 'layout animations', ensure the CSS supports these concepts with appropriate visual feedback or structure.
Return ONLY the style rules with no additional explanations.`,
};
}
else {
return {
systemPrompt: baseSystemPrompt,
userPrompt: `Generate this file based on the following requirements:
Component Name: ${targetFileName}
Description: ${baseConfig.aiDescription}
Component Type: ${baseConfig.componentType}
Target File Name: ${targetFileName}
${importsPrompt}
${componentsUsageExample}
Key Requirements:
1. Create a complete file that matches the file type
2. Maintain consistent patterns with the project
3. Include all necessary functionality
4. Properly integrate all imported components if any
Return ONLY the file content with no additional explanations.`,
};
}
});
}
function generateWithGemini(model, systemPrompt, userPrompt) {
return __awaiter(this, void 0, void 0, function* () {
try {
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
const result = yield model.generateContent(fullPrompt);
const response = yield result.response;
const text = response.text().trim();
return text.replace(/^```[a-z]*\n/, "").replace(/\n```$/, "");
}
catch (error) {
console.error("Error generating file with Gemini:", error);
return "";
}
});
}