UNPKG

invoice-craft

Version:

Customizable, browser-first invoice PDF generator library with modern TypeScript API

355 lines (354 loc) 11.9 kB
export class PluginManager { constructor() { this.plugins = new Map(); this.hooks = { beforeRender: [], afterRender: [], beforeValidation: [], afterValidation: [] }; } register(plugin) { if (this.plugins.has(plugin.name)) { throw new Error(`Plugin '${plugin.name}' is already registered`); } this.plugins.set(plugin.name, plugin); // Register hooks if (plugin.beforeRender) { this.hooks.beforeRender.push(plugin); } if (plugin.afterRender) { this.hooks.afterRender.push(plugin); } if (plugin.beforeValidation) { this.hooks.beforeValidation.push(plugin); } if (plugin.afterValidation) { this.hooks.afterValidation.push(plugin); } } unregister(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { return false; } this.plugins.delete(pluginName); // Remove from hooks this.hooks.beforeRender = this.hooks.beforeRender.filter(p => p.name !== pluginName); this.hooks.afterRender = this.hooks.afterRender.filter(p => p.name !== pluginName); this.hooks.beforeValidation = this.hooks.beforeValidation.filter(p => p.name !== pluginName); this.hooks.afterValidation = this.hooks.afterValidation.filter(p => p.name !== pluginName); return true; } getPlugin(name) { return this.plugins.get(name); } getAllPlugins() { return Array.from(this.plugins.values()); } async executeBeforeRender(invoice) { let result = invoice; for (const plugin of this.hooks.beforeRender) { try { if (plugin.beforeRender) { result = await plugin.beforeRender(result); } } catch (error) { console.error(`Plugin '${plugin.name}' beforeRender hook failed:`, error); // Continue with other plugins } } return result; } async executeAfterRender(pdf) { let result = pdf; for (const plugin of this.hooks.afterRender) { try { if (plugin.afterRender) { result = await plugin.afterRender(result); } } catch (error) { console.error(`Plugin '${plugin.name}' afterRender hook failed:`, error); // Continue with other plugins } } return result; } async executeBeforeValidation(invoice) { let result = invoice; for (const plugin of this.hooks.beforeValidation) { try { if (plugin.beforeValidation) { result = await plugin.beforeValidation(result); } } catch (error) { console.error(`Plugin '${plugin.name}' beforeValidation hook failed:`, error); // Continue with other plugins } } return result; } async executeAfterValidation(validationResult) { let result = validationResult; for (const plugin of this.hooks.afterValidation) { try { if (plugin.afterValidation) { result = await plugin.afterValidation(result); } } catch (error) { console.error(`Plugin '${plugin.name}' afterValidation hook failed:`, error); // Continue with other plugins } } return result; } clear() { this.plugins.clear(); this.hooks = { beforeRender: [], afterRender: [], beforeValidation: [], afterValidation: [] }; } } // Global plugin manager instance export const pluginManager = new PluginManager(); // Plugin creation helpers export function createPlugin(config) { return { name: config.name, version: config.version || '1.0.0', beforeRender: config.beforeRender, afterRender: config.afterRender, beforeValidation: config.beforeValidation, afterValidation: config.afterValidation }; } // Built-in plugins export const builtInPlugins = { // Auto-numbering plugin autoNumbering: createPlugin({ name: 'auto-numbering', version: '1.0.0', beforeRender: (invoice) => { if (!invoice.invoiceNumber || invoice.invoiceNumber === 'AUTO') { const timestamp = Date.now(); const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); invoice.invoiceNumber = `INV-${timestamp}-${random}`; } return invoice; } }), // Currency formatter plugin currencyFormatter: createPlugin({ name: 'currency-formatter', version: '1.0.0', beforeRender: (invoice) => { // Ensure currency is uppercase if (invoice.currency) { invoice.currency = invoice.currency.toUpperCase(); } else { invoice.currency = 'USD'; } return invoice; } }), // Date validator plugin dateValidator: createPlugin({ name: 'date-validator', version: '1.0.0', beforeValidation: (invoice) => { const today = new Date().toISOString().split('T')[0]; // Set invoice date to today if not provided if (!invoice.invoiceDate) { invoice.invoiceDate = today; } // Set due date to 30 days from invoice date if not provided if (!invoice.dueDate && invoice.invoiceDate) { const invoiceDate = new Date(invoice.invoiceDate); const dueDate = new Date(invoiceDate.getTime() + 30 * 24 * 60 * 60 * 1000); invoice.dueDate = dueDate.toISOString().split('T')[0]; } return invoice; } }), // Tax calculator plugin taxCalculator: createPlugin({ name: 'tax-calculator', version: '1.0.0', beforeRender: (invoice) => { // Ensure all items have tax rates invoice.items = invoice.items.map(item => { var _a; return ({ ...item, taxRate: (_a = item.taxRate) !== null && _a !== void 0 ? _a : 0.1 // Default 10% tax if not specified }); }); return invoice; } }), // Address formatter plugin addressFormatter: createPlugin({ name: 'address-formatter', version: '1.0.0', beforeRender: (invoice) => { // Format addresses consistently if (invoice.from.address) { invoice.from.address = invoice.from.address.trim().replace(/\n+/g, '\n'); } if (invoice.to.address) { invoice.to.address = invoice.to.address.trim().replace(/\n+/g, '\n'); } return invoice; } }), // Validation enhancer plugin validationEnhancer: createPlugin({ name: 'validation-enhancer', version: '1.0.0', afterValidation: (result) => { // Add custom business rule validations const customWarnings = []; // Add warning if no terms are specified if (!result.warnings.some(w => w.field === 'terms')) { customWarnings.push({ field: 'terms', message: 'Consider adding payment terms for clarity', code: 'MISSING_TERMS', severity: 'warning' }); } return { ...result, warnings: [...result.warnings, ...customWarnings] }; } }), // Logo optimizer plugin logoOptimizer: createPlugin({ name: 'logo-optimizer', version: '1.0.0', beforeRender: async (invoice) => { // Optimize logo URLs (placeholder implementation) if (invoice.from.logoUrl && !invoice.from.logoUrl.startsWith('data:')) { // In a real implementation, you might compress or resize the image console.log(`Optimizing logo: ${invoice.from.logoUrl}`); } return invoice; } }), // Audit trail plugin auditTrail: createPlugin({ name: 'audit-trail', version: '1.0.0', beforeRender: (invoice) => { // Add audit information invoice._audit = { generatedAt: new Date().toISOString(), generatedBy: 'invoice-craft', version: '1.0.0' }; return invoice; }, afterRender: (pdf) => { console.log('PDF generated with audit trail'); return pdf; } }) }; // Plugin utilities export function registerBuiltInPlugins() { Object.values(builtInPlugins).forEach(plugin => { try { pluginManager.register(plugin); } catch (error) { console.warn(`Failed to register built-in plugin '${plugin.name}':`, error); } }); } export function createPluginChain(plugins) { return { executeBeforeRender: async (invoice) => { let result = invoice; for (const plugin of plugins) { if (plugin.beforeRender) { result = await plugin.beforeRender(result); } } return result; }, executeAfterRender: async (pdf) => { let result = pdf; for (const plugin of plugins) { if (plugin.afterRender) { result = await plugin.afterRender(result); } } return result; }, executeBeforeValidation: async (invoice) => { let result = invoice; for (const plugin of plugins) { if (plugin.beforeValidation) { result = await plugin.beforeValidation(result); } } return result; }, executeAfterValidation: async (result) => { let validationResult = result; for (const plugin of plugins) { if (plugin.afterValidation) { validationResult = await plugin.afterValidation(validationResult); } } return validationResult; } }; } // Plugin development helpers export function validatePlugin(plugin) { const errors = []; if (!plugin.name || typeof plugin.name !== 'string') { errors.push('Plugin must have a valid name'); } if (plugin.version && typeof plugin.version !== 'string') { errors.push('Plugin version must be a string'); } const hasHooks = !!(plugin.beforeRender || plugin.afterRender || plugin.beforeValidation || plugin.afterValidation); if (!hasHooks) { errors.push('Plugin must implement at least one hook'); } return { isValid: errors.length === 0, errors }; } export function getPluginInfo(plugin) { const hooks = []; if (plugin.beforeRender) hooks.push('beforeRender'); if (plugin.afterRender) hooks.push('afterRender'); if (plugin.beforeValidation) hooks.push('beforeValidation'); if (plugin.afterValidation) hooks.push('afterValidation'); return { name: plugin.name, version: plugin.version || '1.0.0', hooks, description: `Plugin with ${hooks.length} hook(s): ${hooks.join(', ')}` }; }