@454creative/easy-email
Version:
A framework-agnostic email service library for Node.js with observability and monitoring
159 lines (137 loc) • 5.09 kB
text/typescript
import fs from 'fs';
import path from 'path';
import { TemplateError, TemplateNotFoundError, InvalidTemplateError } from '../errors';
import { Logger } from '../utils/logger';
export interface TemplateOptions {
engine?: 'handlebars' | 'ejs' | 'pug';
cache?: boolean;
defaultLocale?: string;
}
export class TemplateEngine {
private templates: Map<string, string> = new Map();
private compiledTemplates: Map<string, Function> = new Map();
private options: Required<TemplateOptions>;
private logger: Logger;
constructor(
private templatesDir: string,
options: TemplateOptions = {}
) {
this.options = {
engine: options.engine || 'handlebars',
cache: options.cache ?? true,
defaultLocale: options.defaultLocale || 'en',
};
this.logger = Logger.getInstance();
this.validateTemplatesDir();
}
private validateTemplatesDir(): void {
if (!fs.existsSync(this.templatesDir)) {
throw new TemplateError(`Templates directory not found: ${this.templatesDir}`);
}
}
private getTemplatePath(name: string, locale?: string): string {
const localeDir = locale || this.options.defaultLocale;
const templatePath = path.join(this.templatesDir, localeDir, `${name}.${this.options.engine}`);
if (!fs.existsSync(templatePath)) {
throw new TemplateNotFoundError(name, { locale, path: templatePath });
}
return templatePath;
}
private loadTemplate(name: string, locale?: string): string {
const cacheKey = `${locale || this.options.defaultLocale}:${name}`;
if (this.options.cache && this.templates.has(cacheKey)) {
return this.templates.get(cacheKey)!;
}
const templatePath = this.getTemplatePath(name, locale);
try {
const template = fs.readFileSync(templatePath, 'utf-8');
if (this.options.cache) {
this.templates.set(cacheKey, template);
}
return template;
} catch (error) {
throw new TemplateError(`Failed to load template: ${name}`, { error });
}
}
private compileTemplate(template: string): Function {
try {
switch (this.options.engine) {
case 'handlebars':
return this.compileHandlebars(template);
case 'ejs':
return this.compileEjs(template);
case 'pug':
return this.compilePug(template);
default:
throw new InvalidTemplateError(`Unsupported template engine: ${this.options.engine}`);
}
} catch (error) {
throw new InvalidTemplateError(`Failed to compile template`, { error });
}
}
private compileHandlebars(template: string): Function {
// TODO: In a real implementation, you would import and use the Handlebars library
return (context: any) => {
return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
return context[key.trim()] || '';
});
};
}
private compileEjs(template: string): Function {
// TODO: In a real implementation, you would import and use the EJS library
return (context: any) => {
return template.replace(/<%=([^%>]+)%>/g, (_match, key) => {
return context[key.trim()] || '';
});
};
}
private compilePug(_template: string): Function {
// TODO: In a real implementation, you would import and use the Pug library
throw new Error('Pug compilation not implemented');
}
render(name: string, context: Record<string, any>, locale?: string): string {
this.logger.debug(`Rendering template: ${name}`, { locale, context });
try {
const template = this.loadTemplate(name, locale);
const cacheKey = `${locale || this.options.defaultLocale}:${name}`;
let compiledTemplate: Function;
if (this.options.cache && this.compiledTemplates.has(cacheKey)) {
compiledTemplate = this.compiledTemplates.get(cacheKey)!;
} else {
compiledTemplate = this.compileTemplate(template);
if (this.options.cache) {
this.compiledTemplates.set(cacheKey, compiledTemplate);
}
}
const result = compiledTemplate(context);
this.logger.debug(`Template rendered successfully: ${name}`);
return result;
} catch (error) {
this.logger.error(`Failed to render template: ${name}`, { error, locale, context });
throw error;
}
}
clearCache(): void {
this.templates.clear();
this.compiledTemplates.clear();
this.logger.debug('Template cache cleared');
}
setOptions(options: Partial<TemplateOptions>): void {
this.options = {
...this.options,
...options,
};
this.logger.debug('Template options updated', { options });
}
getAvailableTemplates(locale?: string): string[] {
const localeDir = path.join(this.templatesDir, locale || this.options.defaultLocale);
try {
return fs
.readdirSync(localeDir)
.filter(file => file.endsWith(`.${this.options.engine}`))
.map(file => path.basename(file, `.${this.options.engine}`));
} catch (error) {
throw new TemplateError(`Failed to list templates`, { error, locale });
}
}
}