UNPKG

@454creative/easy-email

Version:

A framework-agnostic email service library for Node.js with observability and monitoring

159 lines (137 loc) 5.09 kB
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 }); } } }