UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

336 lines (281 loc) 9.78 kB
/** * Template Engine for CFN-Forge * * Manages project templates, including listing, copying, and customizing */ const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); const { promisify } = require('util'); const ncp = promisify(require('ncp').ncp); class TemplateEngine { constructor() { this.templatesDir = process.env.CFN_FORGE_TEMPLATES || path.join(__dirname, '../../../templates'); const homeDir = process.env.CFN_FORGE_HOME || path.join(require('os').homedir(), '.cfn-forge'); this.userTemplatesDir = path.join(homeDir, 'templates'); } /** * List all available templates */ async listTemplates() { const templates = []; // Load built-in templates const builtInTemplates = await this.loadTemplatesFromDir(this.templatesDir); templates.push(...builtInTemplates.map((t) => ({ ...t, source: 'built-in' }))); // Load user templates try { const userTemplates = await this.loadTemplatesFromDir(this.userTemplatesDir); templates.push(...userTemplates.map((t) => ({ ...t, source: 'user' }))); } catch { // User templates directory might not exist } // Sort by name templates.sort((a, b) => a.name.localeCompare(b.name)); return templates; } /** * Load templates from a directory */ async loadTemplatesFromDir(dir) { const templates = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const templatePath = path.join(dir, entry.name); const metadataPath = path.join(templatePath, 'template.yaml'); try { const metadata = await this.loadTemplateMetadata(metadataPath); templates.push({ name: entry.name, path: templatePath, ...metadata, }); } catch { // Template without metadata - use defaults templates.push({ name: entry.name, path: templatePath, description: `${entry.name} template`, version: '1.0.0', author: 'unknown', }); } } } } catch (error) { console.error(`Failed to load templates from ${dir}: ${error.message}`); } return templates; } /** * Load template metadata */ async loadTemplateMetadata(metadataPath) { const content = await fs.readFile(metadataPath, 'utf8'); return yaml.load(content); } /** * Copy template to destination */ async copyTemplate(templateName, destination, variables = {}) { // Find template const templates = await this.listTemplates(); const template = templates.find((t) => t.name === templateName); if (!template) { throw new Error(`Template '${templateName}' not found`); } // Copy files await this.copyTemplateFiles(template.path, destination, variables); // Run post-install hooks if any if (template.hooks?.postInstall) { await this.runPostInstallHooks(template.hooks.postInstall, destination, variables); } return template; } /** * Copy template files with variable substitution */ async copyTemplateFiles(source, destination, variables) { const entries = await fs.readdir(source, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(source, entry.name); const destPath = path.join(destination, entry.name); // Skip template metadata file if (entry.name === 'template.yaml') { continue; } if (entry.isDirectory()) { await fs.mkdir(destPath, { recursive: true }); await this.copyTemplateFiles(sourcePath, destPath, variables); } else { await this.copyTemplateFile(sourcePath, destPath, variables); } } } /** * Copy a single template file with variable substitution */ async copyTemplateFile(source, destination, variables) { const ext = path.extname(source).toLowerCase(); const textExtensions = ['.yaml', '.yml', '.json', '.js', '.ts', '.md', '.txt', '.sh', '.env']; if (textExtensions.includes(ext)) { // Process text files let content = await fs.readFile(source, 'utf8'); // Replace variables content = this.replaceVariables(content, variables); await fs.writeFile(destination, content); } else { // Copy binary files as-is await fs.copyFile(source, destination); } // Preserve file permissions const stats = await fs.stat(source); await fs.chmod(destination, stats.mode); } /** * Replace template variables */ replaceVariables(content, variables) { // Replace {{variable}} syntax return content.replace(/\{\{(\w+)\}\}/g, (match, varName) => variables[varName] || match); } /** * Run post-install hooks */ async runPostInstallHooks(hooks, destination, variables) { const { execSync } = require('child_process'); for (const hook of hooks) { if (typeof hook === 'string') { // Simple command const command = this.replaceVariables(hook, variables); execSync(command, { cwd: destination, stdio: 'inherit' }); } else if (hook.type === 'npm' && hook.command) { // npm command execSync(`npm ${hook.command}`, { cwd: destination, stdio: 'inherit' }); } else if (hook.type === 'file' && hook.path) { // Run file const filePath = path.join(destination, hook.path); execSync(`node ${filePath}`, { cwd: destination, stdio: 'inherit' }); } } } /** * Download template from remote repository */ async downloadTemplate(repoUrl, templateName) { const tempDir = path.join(require('os').tmpdir(), `cfn-forge-template-${Date.now()}`); const { execSync } = require('child_process'); try { // Clone repository execSync(`git clone --depth 1 ${repoUrl} ${tempDir}`, { stdio: 'ignore' }); // Copy to user templates const destDir = path.join(this.userTemplatesDir, templateName); await fs.mkdir(this.userTemplatesDir, { recursive: true }); await ncp(tempDir, destDir); // Clean up await fs.rm(tempDir, { recursive: true, force: true }); return destDir; } catch (error) { // Clean up on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch {} throw error; } } /** * Create a new template from existing project */ async createTemplate(projectPath, templateName, metadata = {}) { const templateDir = path.join(this.userTemplatesDir, templateName); // Create template directory await fs.mkdir(templateDir, { recursive: true }); // Copy project files const excludes = [ 'node_modules', '.git', '.env', 'dist', 'build', '.cfn-forge', 'deploy/.env*', ]; await this.copyProjectAsTemplate(projectPath, templateDir, excludes); // Create template metadata const templateMetadata = { name: templateName, description: metadata.description || `${templateName} template`, version: metadata.version || '1.0.0', author: metadata.author || 'unknown', created: new Date().toISOString(), cfnForgeVersion: require('../../../package.json').version, ...metadata, }; const metadataPath = path.join(templateDir, 'template.yaml'); await fs.writeFile(metadataPath, yaml.dump(templateMetadata)); return templateDir; } /** * Copy project as template, excluding certain files */ async copyProjectAsTemplate(source, destination, excludes) { const entries = await fs.readdir(source, { withFileTypes: true }); for (const entry of entries) { // Skip excluded files/directories if (excludes.some((exclude) => entry.name.match(exclude))) { continue; } const sourcePath = path.join(source, entry.name); const destPath = path.join(destination, entry.name); if (entry.isDirectory()) { await fs.mkdir(destPath, { recursive: true }); await this.copyProjectAsTemplate(sourcePath, destPath, excludes); } else { // Template-ize certain files if (entry.name === '.cfn-forge.yaml') { await this.templateizeConfigFile(sourcePath, destPath); } else { await fs.copyFile(sourcePath, destPath); } } } } /** * Convert project config to template config */ async templateizeConfigFile(source, destination) { const content = await fs.readFile(source, 'utf8'); const config = yaml.load(content); // Replace specific values with template variables if (config.project?.name) { config.project.name = '{{projectName}}'; } if (config.project?.description) { config.project.description = '{{description}}'; } if (config.environments) { for (const env of Object.values(config.environments)) { if (env.stackName) { env.stackName = env.stackName.replace( config.project?.name || '', '{{projectName}}', ); } } } await fs.writeFile(destination, yaml.dump(config)); } } // Create singleton instance const engine = new TemplateEngine(); // Export functions for backward compatibility module.exports = { engine, listTemplates: () => engine.listTemplates(), copyTemplate: (name, dest, vars) => engine.copyTemplate(name, dest, vars), downloadTemplate: (url, name) => engine.downloadTemplate(url, name), createTemplate: (path, name, meta) => engine.createTemplate(path, name, meta), };