@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
JavaScript
;
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;
}