@sprouted/create-vibes
Version:
Create a new Vibes monorepo
275 lines (232 loc) • 7.82 kB
text/typescript
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);
}
}