invoice-craft
Version:
Customizable, browser-first invoice PDF generator library with modern TypeScript API
355 lines (354 loc) • 11.9 kB
JavaScript
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(', ')}`
};
}