UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

744 lines (743 loc) 30.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TemplateEngine = exports.HookType = exports.TemplateCategory = void 0; exports.createTemplateEngine = createTemplateEngine; exports.getGlobalTemplateEngine = getGlobalTemplateEngine; exports.setGlobalTemplateEngine = setGlobalTemplateEngine; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const handlebars = __importStar(require("handlebars")); const yaml = __importStar(require("js-yaml")); var TemplateCategory; (function (TemplateCategory) { TemplateCategory["MICROFRONTEND"] = "microfrontend"; TemplateCategory["BACKEND"] = "backend"; TemplateCategory["FULLSTACK"] = "fullstack"; TemplateCategory["LIBRARY"] = "library"; TemplateCategory["APPLICATION"] = "application"; TemplateCategory["SERVICE"] = "service"; TemplateCategory["COMPONENT"] = "component"; TemplateCategory["CONFIGURATION"] = "configuration"; TemplateCategory["INFRASTRUCTURE"] = "infrastructure"; TemplateCategory["TESTING"] = "testing"; TemplateCategory["DOCUMENTATION"] = "documentation"; TemplateCategory["CUSTOM"] = "custom"; })(TemplateCategory || (exports.TemplateCategory = TemplateCategory = {})); var HookType; (function (HookType) { HookType["BEFORE_INSTALL"] = "before_install"; HookType["AFTER_INSTALL"] = "after_install"; HookType["BEFORE_PROCESS"] = "before_process"; HookType["AFTER_PROCESS"] = "after_process"; HookType["BEFORE_FILE"] = "before_file"; HookType["AFTER_FILE"] = "after_file"; HookType["VALIDATE"] = "validate"; HookType["CLEANUP"] = "cleanup"; })(HookType || (exports.HookType = HookType = {})); class TemplateEngine extends events_1.EventEmitter { constructor(templatePaths = [], options = {}) { super(); this.templatePaths = templatePaths; this.options = options; this.templates = new Map(); this.templateCache = new Map(); this.handlebarsInstance = handlebars.create(); this.registerBuiltinHelpers(); this.registerCustomHelpers(); this.loadTemplates(); } registerBuiltinHelpers() { // String helpers this.handlebarsInstance.registerHelper('lowercase', (str) => str?.toLowerCase()); this.handlebarsInstance.registerHelper('uppercase', (str) => str?.toUpperCase()); this.handlebarsInstance.registerHelper('capitalize', (str) => str?.charAt(0).toUpperCase() + str?.slice(1)); this.handlebarsInstance.registerHelper('camelCase', (str) => str?.replace(/[-_\s]+(.)?/g, (_, c) => c?.toUpperCase() || '')); this.handlebarsInstance.registerHelper('kebabCase', (str) => str?.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`).replace(/^-/, '')); this.handlebarsInstance.registerHelper('snakeCase', (str) => str?.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, '')); // Logic helpers this.handlebarsInstance.registerHelper('eq', (a, b) => a === b); this.handlebarsInstance.registerHelper('ne', (a, b) => a !== b); this.handlebarsInstance.registerHelper('lt', (a, b) => a < b); this.handlebarsInstance.registerHelper('gt', (a, b) => a > b); this.handlebarsInstance.registerHelper('lte', (a, b) => a <= b); this.handlebarsInstance.registerHelper('gte', (a, b) => a >= b); this.handlebarsInstance.registerHelper('and', (...args) => { const values = args.slice(0, -1); // Remove options object return values.every(v => v); }); this.handlebarsInstance.registerHelper('or', (...args) => { const values = args.slice(0, -1); // Remove options object return values.some(v => v); }); this.handlebarsInstance.registerHelper('not', (value) => !value); // Array helpers this.handlebarsInstance.registerHelper('includes', (array, value) => Array.isArray(array) && array.includes(value)); this.handlebarsInstance.registerHelper('join', (array, separator) => Array.isArray(array) ? array.join(separator || ', ') : ''); // Date helpers this.handlebarsInstance.registerHelper('year', () => new Date().getFullYear()); this.handlebarsInstance.registerHelper('date', () => new Date().toISOString()); // JSON helpers this.handlebarsInstance.registerHelper('json', (obj) => JSON.stringify(obj, null, 2)); this.handlebarsInstance.registerHelper('jsonParse', (str) => { try { return JSON.parse(str); } catch { return null; } }); } registerCustomHelpers() { if (this.options.customHelpers) { for (const [name, helper] of Object.entries(this.options.customHelpers)) { this.handlebarsInstance.registerHelper(name, helper); } } } async loadTemplates() { for (const templatePath of this.templatePaths) { try { if (await fs.pathExists(templatePath)) { await this.loadTemplatesFromDirectory(templatePath); } } catch (error) { this.emit('error', { type: 'load', path: templatePath, error }); } } // Load remote templates if enabled if (this.options.enableRemoteTemplates && this.options.templateRegistry) { await this.loadRemoteTemplates(); } } async loadTemplatesFromDirectory(directory) { const entries = await fs.readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const templatePath = path.join(directory, entry.name); const manifestPath = path.join(templatePath, 'template.yaml'); if (await fs.pathExists(manifestPath)) { try { const manifest = await fs.readFile(manifestPath, 'utf8'); const template = yaml.load(manifest); // Validate and enhance template template.id = template.id || entry.name; template.metadata = { ...template.metadata, created: new Date(template.metadata?.created || Date.now()), updated: new Date(template.metadata?.updated || Date.now()) }; // Resolve file paths template.files = template.files.map(file => ({ ...file, source: path.isAbsolute(file.source) ? file.source : path.join(templatePath, file.source) })); this.templates.set(template.id, template); this.emit('template:loaded', template); } catch (error) { this.emit('error', { type: 'parse', path: manifestPath, error }); } } } } } async loadRemoteTemplates() { // TODO: Implement remote template loading from registry this.emit('info', 'Remote template loading not yet implemented'); } async registerTemplate(template) { // Validate template const validation = await this.validateTemplate(template); if (!validation.valid) { throw new Error(`Invalid template: ${validation.errors.join(', ')}`); } this.templates.set(template.id, template); this.emit('template:registered', template); } async processTemplate(templateId, context) { const template = this.templates.get(templateId); if (!template) { throw new Error(`Template '${templateId}' not found`); } // Build complete context const fullContext = await this.buildContext(template, context); // Process inheritance chain const { mergedTemplate, inheritanceChain } = await this.resolveInheritance(template); // Process interfaces const interfaceTemplates = await this.resolveInterfaces(mergedTemplate); // Merge variables from inheritance chain and interfaces const mergedVariables = await this.mergeVariables(mergedTemplate, inheritanceChain, interfaceTemplates, fullContext.variables); // Update context with merged data fullContext.variables = mergedVariables; fullContext.template = mergedTemplate; fullContext.parentTemplates = inheritanceChain.slice(1).map(id => this.templates.get(id)); fullContext.interfaces = interfaceTemplates; // Initialize result const result = { template: mergedTemplate, mergedVariables, processedFiles: [], executedHooks: [], inheritanceChain: inheritanceChain.map(t => t), warnings: [], errors: [] }; try { // Execute before hooks await this.executeHooks(mergedTemplate, HookType.BEFORE_PROCESS, fullContext, result); // Process files for (const file of mergedTemplate.files) { await this.processFile(file, fullContext, result); } // Execute after hooks await this.executeHooks(mergedTemplate, HookType.AFTER_PROCESS, fullContext, result); // Cache if enabled if (this.options.enableCache) { const cacheKey = this.getCacheKey(templateId, fullContext); this.templateCache.set(cacheKey, result); } this.emit('template:processed', result); return result; } catch (error) { result.errors.push(error.message); this.emit('template:error', { template, error, result }); throw error; } } async buildContext(template, partial) { return { variables: partial.variables || {}, template, projectPath: partial.projectPath || process.cwd(), timestamp: new Date(), user: partial.user || { name: process.env.USER || process.env.USERNAME, email: process.env.GIT_AUTHOR_EMAIL }, system: { platform: process.platform, arch: process.arch, nodeVersion: process.version }, ...partial }; } async resolveInheritance(template, visited = new Set()) { const inheritanceChain = [template.id]; if (visited.has(template.id)) { throw new Error(`Circular inheritance detected: ${template.id}`); } visited.add(template.id); if (!template.extends || template.extends.length === 0) { return { mergedTemplate: template, inheritanceChain }; } // Deep clone template let mergedTemplate = JSON.parse(JSON.stringify(template)); // Process each parent template for (const parentId of template.extends) { const parentTemplate = this.templates.get(parentId); if (!parentTemplate) { throw new Error(`Parent template '${parentId}' not found`); } // Recursively resolve parent's inheritance const { mergedTemplate: resolvedParent, inheritanceChain: parentChain } = await this.resolveInheritance(parentTemplate, visited); // Merge parent into current template mergedTemplate = this.mergeTemplates(resolvedParent, mergedTemplate); inheritanceChain.push(...parentChain); } return { mergedTemplate, inheritanceChain: [...new Set(inheritanceChain)] }; } async resolveInterfaces(template) { if (!template.implements || template.implements.length === 0) { return []; } const interfaces = []; for (const interfaceId of template.implements) { const interfaceTemplate = this.templates.get(interfaceId); if (!interfaceTemplate) { throw new Error(`Interface template '${interfaceId}' not found`); } interfaces.push(interfaceTemplate); } return interfaces; } mergeTemplates(parent, child) { // Deep merge templates with child taking precedence const merged = { ...parent, ...child, variables: this.mergeArrays(parent.variables, child.variables, 'name'), files: this.mergeArrays(parent.files, child.files, 'destination'), hooks: [...parent.hooks, ...child.hooks], tags: [...new Set([...parent.tags, ...child.tags])], requires: this.mergeArrays(parent.requires || [], child.requires || [], 'name') }; return merged; } mergeArrays(parent, child, keyField) { const merged = new Map(); // Add parent items for (const item of parent) { merged.set(item[keyField], item); } // Override with child items for (const item of child) { merged.set(item[keyField], item); } return Array.from(merged.values()); } async mergeVariables(template, inheritanceChain, interfaces, userVariables) { const merged = {}; // Start with template defaults for (const variable of template.variables) { if (variable.default !== undefined) { merged[variable.name] = variable.default; } } // Apply interface requirements for (const interfaceTemplate of interfaces) { for (const variable of interfaceTemplate.variables) { if (variable.required && !(variable.name in merged)) { throw new Error(`Interface '${interfaceTemplate.id}' requires variable '${variable.name}'`); } } } // Apply user overrides for (const [key, value] of Object.entries(userVariables)) { merged[key] = value; } // Validate all required variables are present for (const variable of template.variables) { if (variable.required && !(variable.name in merged)) { throw new Error(`Required variable '${variable.name}' not provided`); } // Apply transformations if (variable.transform && variable.name in merged) { merged[variable.name] = await this.transformValue(merged[variable.name], variable.transform); } // Validate value if (variable.name in merged) { const validation = await this.validateVariable(variable, merged[variable.name]); if (!validation.valid) { throw new Error(`Invalid value for '${variable.name}': ${validation.error}`); } } } return merged; } async transformValue(value, transform) { // Execute transformation function try { const fn = new Function('value', transform); return fn(value); } catch (error) { throw new Error(`Transform failed: ${error.message}`); } } async validateVariable(variable, value) { // Type validation const actualType = Array.isArray(value) ? 'array' : typeof value; if (actualType !== variable.type && variable.type !== 'choice') { return { valid: false, error: `Expected ${variable.type}, got ${actualType}` }; } // Choice validation if (variable.type === 'choice' && variable.choices) { if (!variable.choices.includes(value)) { return { valid: false, error: `Must be one of: ${variable.choices.join(', ')}` }; } } // Pattern validation if (variable.pattern && typeof value === 'string') { const regex = new RegExp(variable.pattern); if (!regex.test(value)) { return { valid: false, error: `Does not match pattern: ${variable.pattern}` }; } } // Custom validation if (variable.validate) { try { const fn = new Function('value', variable.validate); const result = fn(value); if (result !== true) { return { valid: false, error: typeof result === 'string' ? result : 'Validation failed' }; } } catch (error) { return { valid: false, error: `Validation error: ${error.message}` }; } } return { valid: true }; } async processFile(file, context, result) { // Check condition if (file.condition) { const shouldProcess = await this.evaluateCondition(file.condition, context); if (!shouldProcess) { return; } } const processedFile = { source: file.source, destination: this.resolvePath(file.destination, context), content: undefined, processed: false, error: undefined }; try { // Execute before file hook await this.executeHooks(context.template, HookType.BEFORE_FILE, { ...context, file }, result); // Read source file const sourceContent = await fs.readFile(file.source, (file.encoding || 'utf8')); // Process content based on transform type let processedContent; switch (file.transform || 'handlebars') { case 'handlebars': processedContent = this.handlebarsInstance.compile(sourceContent)(context.variables); break; case 'ejs': // TODO: Implement EJS processing processedContent = sourceContent; break; case 'none': processedContent = sourceContent; break; default: processedContent = sourceContent; } processedFile.content = processedContent; // Handle file writing based on merge strategy const destPath = processedFile.destination; const destDir = path.dirname(destPath); await fs.ensureDir(destDir); if (file.merge && await fs.pathExists(destPath)) { processedContent = await this.mergeFileContent(destPath, processedContent, file.mergeStrategy || 'override', file.mergeCustom); } // Write file await fs.writeFile(destPath, processedContent, (file.encoding || 'utf8')); // Set permissions if specified if (file.permissions) { await fs.chmod(destPath, file.permissions); } processedFile.processed = true; // Execute after file hook await this.executeHooks(context.template, HookType.AFTER_FILE, { ...context, file }, result); } catch (error) { processedFile.error = error.message; result.errors.push(`Failed to process ${file.source}: ${error.message}`); } result.processedFiles.push(processedFile); } async mergeFileContent(existingPath, newContent, strategy, customStrategy) { const existingContent = await fs.readFile(existingPath, 'utf8'); switch (strategy) { case 'override': return newContent; case 'append': return existingContent + '\n' + newContent; case 'prepend': return newContent + '\n' + existingContent; case 'deep': // Try to parse as JSON and deep merge try { const existing = JSON.parse(existingContent); const newData = JSON.parse(newContent); return JSON.stringify(this.deepMerge(existing, newData), null, 2); } catch { // Fall back to append if not JSON return existingContent + '\n' + newContent; } case 'custom': if (customStrategy) { try { const fn = new Function('existing', 'new', customStrategy); return fn(existingContent, newContent); } catch (error) { throw new Error(`Custom merge failed: ${error.message}`); } } return newContent; default: return newContent; } } deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } async executeHooks(template, type, context, result) { const hooks = template.hooks.filter(h => h.type === type); for (const hook of hooks) { // Check condition if (hook.condition) { const shouldExecute = await this.evaluateCondition(hook.condition, context); if (!shouldExecute) { continue; } } const startTime = Date.now(); const executedHook = { hook, success: false, output: undefined, error: undefined, duration: 0 }; try { let output = ''; if (hook.command) { // Execute shell command const { execSync } = require('child_process'); const env = { ...process.env, ...hook.environment, ...this.createHookEnvironment(context) }; output = execSync(hook.command, { cwd: context.projectPath, env, timeout: hook.timeout || 30000, encoding: 'utf8' }); } else if (hook.script) { // Execute JavaScript const fn = new Function('context', 'require', hook.script); const result = await fn(context, require); output = String(result || ''); } executedHook.success = true; executedHook.output = output; } catch (error) { executedHook.error = error.message; if (!hook.allowFailure) { throw error; } result.warnings.push(`Hook '${hook.name}' failed: ${error.message}`); } executedHook.duration = Date.now() - startTime; result.executedHooks.push(executedHook); } } createHookEnvironment(context) { const env = {}; // Add all variables as environment variables for (const [key, value] of Object.entries(context.variables)) { env[`TEMPLATE_VAR_${key.toUpperCase()}`] = String(value); } // Add context information env.TEMPLATE_ID = context.template.id; env.TEMPLATE_NAME = context.template.name; env.TEMPLATE_VERSION = context.template.version; env.PROJECT_PATH = context.projectPath; env.TIMESTAMP = context.timestamp.toISOString(); return env; } async evaluateCondition(condition, context) { try { const fn = new Function('context', `return ${condition}`); return Boolean(fn(context)); } catch (error) { throw new Error(`Invalid condition: ${error.message}`); } } resolvePath(pathTemplate, context) { // Replace variables in path let resolved = pathTemplate; for (const [key, value] of Object.entries(context.variables)) { resolved = resolved.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value)); } // Make path absolute if (!path.isAbsolute(resolved)) { resolved = path.join(context.projectPath, resolved); } return resolved; } getCacheKey(templateId, context) { const variables = JSON.stringify(context.variables); const crypto = require('crypto'); return crypto .createHash('sha256') .update(`${templateId}:${variables}`) .digest('hex'); } async validateTemplate(template) { const errors = []; // Required fields if (!template.id) errors.push('Template ID is required'); if (!template.name) errors.push('Template name is required'); if (!template.version) errors.push('Template version is required'); if (!template.category) errors.push('Template category is required'); if (!template.files || template.files.length === 0) { errors.push('Template must have at least one file'); } // Validate extends if (template.extends) { for (const parentId of template.extends) { if (!this.templates.has(parentId)) { errors.push(`Parent template '${parentId}' not found`); } } } // Validate implements if (template.implements) { for (const interfaceId of template.implements) { if (!this.templates.has(interfaceId)) { errors.push(`Interface template '${interfaceId}' not found`); } } } // Validate files for (const file of template.files || []) { if (!file.source) errors.push('File source is required'); if (!file.destination) errors.push('File destination is required'); } // Validate variables const variableNames = new Set(); for (const variable of template.variables || []) { if (!variable.name) errors.push('Variable name is required'); if (!variable.type) errors.push('Variable type is required'); if (variableNames.has(variable.name)) { errors.push(`Duplicate variable name: ${variable.name}`); } variableNames.add(variable.name); } return { valid: errors.length === 0, errors }; } // Query methods getTemplate(id) { return this.templates.get(id); } getAllTemplates() { return Array.from(this.templates.values()); } getTemplatesByCategory(category) { return Array.from(this.templates.values()) .filter(t => t.category === category); } getTemplatesByTag(tag) { return Array.from(this.templates.values()) .filter(t => t.tags.includes(tag)); } searchTemplates(query) { const lowerQuery = query.toLowerCase(); return Array.from(this.templates.values()) .filter(t => t.name.toLowerCase().includes(lowerQuery) || t.description.toLowerCase().includes(lowerQuery) || t.tags.some(tag => tag.toLowerCase().includes(lowerQuery))); } getInheritanceHierarchy(templateId) { const template = this.templates.get(templateId); if (!template) return []; const hierarchy = [templateId]; const visited = new Set(); const traverse = (id) => { if (visited.has(id)) return; visited.add(id); const t = this.templates.get(id); if (t?.extends) { for (const parentId of t.extends) { hierarchy.push(parentId); traverse(parentId); } } }; traverse(templateId); return hierarchy; } clearCache() { this.templateCache.clear(); this.emit('cache:cleared'); } } exports.TemplateEngine = TemplateEngine; // Global template engine let globalTemplateEngine = null; function createTemplateEngine(templatePaths, options) { return new TemplateEngine(templatePaths, options); } function getGlobalTemplateEngine() { if (!globalTemplateEngine) { globalTemplateEngine = new TemplateEngine(); } return globalTemplateEngine; } function setGlobalTemplateEngine(engine) { globalTemplateEngine = engine; }