UNPKG

@sprouted/create-vibes

Version:
275 lines (232 loc) 7.82 kB
import fs from 'fs-extra'; import path from 'path'; import handlebars from 'handlebars'; import { Template, TemplateConfig, GenerateOptions, Transform, TemplateFile, Feature, FeatureContext } from './types'; export class TemplateEngine { private templates: Map<string, Template> = new Map(); private features: Map<string, Feature> = new Map(); constructor() { // Register custom handlebars helpers this.registerHelpers(); } private registerHelpers() { // Case transformations handlebars.registerHelper('camelCase', (str: string) => str.replace(/-([a-z])/g, g => g[1].toUpperCase()) ); handlebars.registerHelper('pascalCase', (str: string) => { const camel = str.replace(/-([a-z])/g, g => g[1].toUpperCase()); return camel.charAt(0).toUpperCase() + camel.slice(1); }); handlebars.registerHelper('kebabCase', (str: string) => str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() ); // Conditionals handlebars.registerHelper('if_eq', function(this: any, a: any, b: any, options: any) { return a === b ? options.fn(this) : options.inverse(this); }); handlebars.registerHelper('if_includes', function(this: any, array: any[], value: any, options: any) { return array && array.includes(value) ? options.fn(this) : options.inverse(this); }); } async loadTemplate(templatePath: string): Promise<TemplateConfig> { const configPath = path.join(templatePath, 'template.config.json'); if (!await fs.pathExists(configPath)) { throw new Error(`Template configuration not found at ${configPath}`); } const config = await fs.readJson(configPath) as TemplateConfig; // Validate template config this.validateTemplateConfig(config); return config; } private validateTemplateConfig(config: TemplateConfig) { if (!config.name || !config.type || !config.files) { throw new Error('Invalid template configuration: missing required fields'); } } async generateFromTemplate( templatePath: string, variables: Record<string, any>, options: GenerateOptions ): Promise<void> { const config = await this.loadTemplate(templatePath); // Validate variables against template requirements this.validateVariables(config, variables); // Process each file in the template for (const file of config.files) { if (file.condition && !file.condition(variables)) { continue; // Skip this file } await this.processTemplateFile( path.join(templatePath, file.source), path.join(options.outputPath, file.destination), variables, file.transforms, options ); } // Install features if specified if (config.features && variables.features) { await this.installFeatures( options.outputPath, variables.features, variables.packageManager || 'pnpm' ); } } private validateVariables(config: TemplateConfig, variables: Record<string, any>) { for (const varDef of config.variables) { if (varDef.required && !(varDef.name in variables)) { throw new Error(`Missing required variable: ${varDef.name}`); } // Set defaults for missing optional variables if (!(varDef.name in variables) && varDef.default !== undefined) { variables[varDef.name] = varDef.default; } // Validate select options if (varDef.type === 'select' && varDef.options && variables[varDef.name]) { if (!varDef.options.includes(variables[varDef.name])) { throw new Error(`Invalid value for ${varDef.name}: ${variables[varDef.name]}`); } } } } private async processTemplateFile( sourcePath: string, destPath: string, variables: Record<string, any>, transforms?: Transform[], options?: GenerateOptions ): Promise<void> { // Read template file const templateContent = await fs.readFile(sourcePath, 'utf-8'); // Compile and render with handlebars const template = handlebars.compile(templateContent); let rendered = template(variables); // Apply transforms if (transforms) { rendered = this.applyTransforms(rendered, transforms, variables); } // Write file if (!options?.dryRun) { await fs.ensureDir(path.dirname(destPath)); await fs.writeFile(destPath, rendered); } if (!options?.silent) { console.log(`Created: ${destPath}`); } } private applyTransforms( content: string, transforms: Transform[], variables: Record<string, any> ): string { let result = content; for (const transform of transforms) { switch (transform.type) { case 'replace': if (transform.config.pattern && transform.config.replacement) { result = result.replace( transform.config.pattern, transform.config.replacement ); } break; case 'conditional': if (transform.config.condition && !transform.config.condition(variables)) { // Remove conditional blocks or apply alternative transform continue; } break; case 'case': // Apply case transformation to specific patterns // Implementation depends on specific requirements break; } } return result; } async installFeatures( projectPath: string, featureIds: string[], packageManager: string ): Promise<void> { for (const featureId of featureIds) { const feature = this.features.get(featureId); if (!feature) { console.warn(`Unknown feature: ${featureId}`); continue; } const context: FeatureContext = { projectPath, packageManager: packageManager as any, variables: {}, features: featureIds }; // Install dependencies if (feature.dependencies) { await this.installPackages( projectPath, feature.dependencies, false, packageManager ); } if (feature.devDependencies) { await this.installPackages( projectPath, feature.devDependencies, true, packageManager ); } // Run post-install hook if (feature.postInstall) { await feature.postInstall(context); } } } private async installPackages( projectPath: string, packages: string[], dev: boolean, packageManager: string ): Promise<void> { // Implementation would use child_process to run package manager commands const command = this.getInstallCommand(packageManager, packages, dev); // Execute command in project directory // This is a placeholder - actual implementation would use execSync or similar console.log(`Installing: ${packages.join(', ')}`); } private getInstallCommand( packageManager: string, packages: string[], dev: boolean ): string { const packageList = packages.join(' '); switch (packageManager) { case 'npm': return `npm install ${dev ? '--save-dev' : ''} ${packageList}`; case 'yarn': return `yarn add ${dev ? '--dev' : ''} ${packageList}`; case 'pnpm': return `pnpm add ${dev ? '--save-dev' : ''} ${packageList}`; default: throw new Error(`Unknown package manager: ${packageManager}`); } } registerFeature(feature: Feature): void { this.features.set(feature.id, feature); } registerTemplate(template: Template): void { this.templates.set(template.name, template); } }