npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@simplicate/sigil

v2.2.1

Published

Sigil - A structured error handling library that acts as a beacon for potential issues in your codebase

Downloads

24

Readme

Sigil (@simplicate/sigil)

A structured error handling library that acts as a beacon for potential issues in your codebase. Sigil provides a flexible and extensible way to process errors in your Node.js/TypeScript applications using the strategy pattern, with built-in support for structured logging and severity-based classification.

What is a Sigil?

A sigil is a personal, symbolic representation of a specific intention or desire, created by condensing an idea into a unique pictographic form. The process involves writing out a goal in words, then removing vowels and repeated letters before combining the remaining letters to form an abstract symbol. The completed sigil is then "charged" with energy through rituals like visualization or meditation and used to manifest the creator's intention.

How does this relate to our error handling library?

Just as a traditional sigil distills a complex intention into a clear, powerful symbol, the Sigil error handling library condenses the complexity of error management into a structured, actionable form. In your codebase, errors can be chaotic and hard to interpret. Sigil acts as a symbolic marker, transforming raw error data into meaningful, structured information that reflects your application's intentions for reliability and clarity.

By defining error handling strategies and structured logging, Sigil helps you "charge" your error handling with purpose: every error is processed, classified, and logged in a way that aligns with your application's goals. This makes error handling not just a reaction, but a deliberate, intention-driven part of your software's design—mirroring the way a sigil is created and used to manifest a specific outcome.

Features

  • Strategy Pattern: Easily define custom error handling strategies for different error types
  • Application-Level: Framework-agnostic error processing that returns structured error data
  • TypeScript Support: Fully typed with TypeScript definitions
  • Structured Logging: Built-in support for structured logging with automatic context enrichment
  • Severity-Based: Configurable error severity levels (low, medium, high, critical)
  • Severity-Aware Logging: Automatic log level selection based on error severity
  • Extensible: Easy to extend with custom strategies for domain-specific error handling

Installation

npm install @simplicate/sigil

Logging

Sigil provides a comprehensive logging system with built-in support for multiple logging frameworks through a unified AppLogger interface. The logging system features automatic framework detection, graceful fallbacks, and structured logging capabilities.

Quick Start

import { createLogger } from '@simplicate/sigil/logging';

// Auto-detect and create the best available logger
const logger = createLogger({ level: 'info', pretty: true });

// Use structured logging
logger.info({ userId: 123, operation: 'login' }, 'User authentication successful');
logger.error({ error: 'INVALID_TOKEN' }, 'Authentication failed');

// Create child loggers with persistent context
const requestLogger = logger.child({ requestId: 'req-456', service: 'auth' });
requestLogger.info('Processing request'); // Automatically includes requestId & service

Logger Factory

The createLogger function provides automatic framework detection and configuration:

import { createLogger } from '@simplicate/sigil/logging';

// Auto-detect best available logger (Pino → Winston → Console)
const logger = createLogger({ level: 'info', pretty: true });

// Create specific logger types
const pinoLogger = createLogger('pino', { level: 'debug', pretty: false });
const winstonLogger = createLogger('winston', { level: 'warn' });
const consoleLogger = createLogger('console', { bindings: { service: 'my-app' } });

Supported Logger Types

  • 'pino': High-performance JSON logger (requires pino package)
  • 'winston': Popular Winston logger (requires winston package)
  • 'console': Native console methods (no dependencies)
  • 'auto': Auto-detects available framework with graceful fallbacks

Configuration Options

interface LoggerConfig {
  level?: 'debug' | 'info' | 'warn' | 'error';  // Log level
  pretty?: boolean;                             // Pretty print (dev mode)
  bindings?: Record<string, unknown>;           // Default context
}

Graceful Fallbacks

The auto-detection system gracefully falls back when dependencies aren't available:

  1. Pino (preferred) - High performance JSON logging
  2. Winston (fallback) - If Pino not available
  3. Console (final fallback) - Always available
// This will never fail - always returns a working logger
const logger = createLogger('auto', { level: 'info' });

Using Existing Logger Instances

If you already have logger instances, use adapters to make them compatible:

import { createPinoAdapter, createWinstonAdapter, createConsoleAdapter } from '@simplicate/sigil/logging';
import pino from 'pino';
import winston from 'winston';

// Pino adapter
const pinoInstance = pino({ level: 'debug' });
const pinoLogger = createPinoAdapter(pinoInstance);

// Winston adapter  
const winstonInstance = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()]
});
const winstonLogger = createWinstonAdapter(winstonInstance);

// Console adapter
const consoleLogger = createConsoleAdapter({ service: 'my-service' });

Structured Logging

All loggers support both structured (object + message) and simple (string-only) logging:

// Structured logging (recommended)
logger.error({ userId: 123, operation: 'payment', amount: 99.99 }, 'Payment processing failed');
logger.info({ requestId: 'req-789', duration: 245 }, 'Request completed');

// Simple string logging
logger.info('Application started');
logger.warn('Rate limit approaching');

Child Loggers

Create contextual loggers that inherit parent bindings:

const logger = createLogger({ bindings: { service: 'user-service' } });

// Child logger inherits 'service' and adds 'userId'
const userLogger = logger.child({ userId: 123 });

// Child of child - inherits all previous context
const operationLogger = userLogger.child({ operation: 'password-reset' });

operationLogger.info('Starting operation'); 
// Logs: { service: 'user-service', userId: 123, operation: 'password-reset', msg: 'Starting operation' }

Integration with Error Handling

Loggers integrate seamlessly with Sigil's error handling strategies:

import { createLogger } from '@simplicate/sigil/logging';
import { sig } from '@simplicate/sigil';

const logger = createLogger('auto', { level: 'info' });

const errorHandler = new sig.GenericErrorHandler([
  new sig.ConfigurableErrorStrategy(ValidationError, {
    code: 'VALIDATION_ERROR',
    message: 'Validation failed',
    severity: 'medium' // Automatically logs at 'warn' level
  }),
  new sig.ConfigurableErrorStrategy(DatabaseError, {
    code: 'DATABASE_ERROR', 
    message: 'Database operation failed',
    severity: 'high' // Automatically logs at 'error' level
  })
]);

try {
  await riskyOperation();
} catch (error) {
  // Error is automatically logged with appropriate level based on severity
  const result = errorHandler.handle(error, logger, {
    operation: 'user-creation',
    userId: 123
  });
}

Logger Compatibility

The AppLogger interface is compatible with popular logging libraries:

  • Pino: Direct compatibility
  • Winston: Compatible with structured logging
  • Bunyan: Compatible interface
  • Custom loggers: Implement the AppLogger interface

No peer dependencies required - use any logger you prefer!

Usage

Basic Usage

import { sig, AppLogger } from '@simplicate/sigil';
import pino from 'pino';

const logger = pino();

// Define custom error classes using sig.AppError base class
class ValidationError extends sig.AppError {
  constructor(message: string, public field?: string) {
    super('ValidationError', message);
  }
}

class DatabaseError extends sig.AppError {
  constructor(message: string, public connectionString?: string) {
    super('DatabaseError', message);
  }
}

// Create error handling strategies
const validationStrategy = new sig.ConfigurableErrorStrategy(ValidationError, {
  code: 'VALIDATION_ERROR',
  message: 'Validation failed',
  severity: 'medium'
});

const databaseStrategy = new sig.ConfigurableErrorStrategy(DatabaseError, {
  code: 'DATABASE_ERROR',
  message: 'Database operation failed',
  severity: 'high'
});

// Create the error handler with strategies
const errorHandler = new sig.GenericErrorHandler([
  validationStrategy,
  databaseStrategy
]);

// Use in your application
try {
  // Some operation that might throw
  await processUserData(userData);
} catch (error) {
  // Process the error and get structured result
  const errorResult = errorHandler.handle(error, logger, {
    operation: 'user-processing',
    userId: userData.id
  });
  
  // Use the structured error result as needed
  console.log('Error Code:', errorResult.code);
  console.log('Severity:', errorResult.severity);
  
  // In a web framework, you might convert severity to HTTP status:
  const httpStatus = severityToHttpStatus(errorResult.severity);
  return { status: httpStatus, error: errorResult };
}

// Helper function for web frameworks
function severityToHttpStatus(severity: string): number {
  switch (severity) {
    case 'low': return 400;
    case 'medium': return 422;
    case 'high': return 500;
    case 'critical': return 503;
    default: return 500;
  }
}

Custom Error Strategy

You can create completely custom error strategies by implementing the ErrorHandlerStrategy interface:

import { ErrorHandlerStrategy, ErrorResult, AppLogger } from '@simplicate/sigil';

class CustomDatabaseErrorStrategy implements ErrorHandlerStrategy {
  canHandle(error: unknown): boolean {
    // Check if this is a database connection error
    return error instanceof Error && error.message.includes('ECONNREFUSED');
  }

  handle(error: unknown, logger: AppLogger, context: Record<string, unknown>): ErrorResult {
    logger.error({ err: error, ...context }, 'Database connection failed');
    
    return {
      code: 'DATABASE_CONNECTION_ERROR',
      message: 'Database service is temporarily unavailable',
      severity: 'critical',
      originalError: error,
      context
    };
  }
}

// Usage with custom strategy
const errorHandler = new sig.GenericErrorHandler([
  new CustomDatabaseErrorStrategy(),
  new sig.DefaultErrorStrategy()
]);

API Reference

GenericErrorHandler

The main error handler class that manages error handling strategies.

constructor(strategies: ErrorHandlerStrategy[], fallbackStrategy?: ErrorHandlerStrategy)
  • strategies: Array of error handling strategies, ordered by priority
  • fallbackStrategy: Optional fallback strategy (defaults to DefaultErrorStrategy)
handle(error: unknown, logger: AppLogger, context?: Record<string, unknown>): ErrorResult
  • error: The error to handle
  • logger: Logger instance implementing AppLogger interface
  • context: Optional context object for logging
  • Returns: ErrorResult - Structured error data

ConfigurableErrorStrategy

A configurable strategy for handling specific error types.

constructor(errorType: new (...args: any[]) => TError, config: ErrorConfig)
  • errorType: The error class constructor
  • config: Error configuration object

DefaultErrorStrategy

A fallback strategy that handles any unmatched errors.

constructor(defaultMessage?: string, defaultCode?: string, defaultSeverity?: SeverityLevel)
  • defaultMessage: Message for unknown errors (default: "An unexpected error occurred.")
  • defaultCode: Error code for unknown errors (default: "UNKNOWN_ERROR")
  • defaultSeverity: Severity level for unknown errors (default: "high")

AppError vs GenericAppError

Sigil provides two error classes to cover different use cases:

AppError (Base Class)

A base class for creating custom application errors with proper stack trace handling.

constructor(name: string, message: string)
  • name: The error name (typically the class name)
  • message: The error message
toJSON(): Record<string, unknown>
  • Returns: JSON serializable representation of the error

Benefits of using AppError:

  • Automatic stack trace optimization using Error.captureStackTrace when available
  • Consistent error name handling
  • Built-in JSON serialization support
  • Reduces boilerplate code in custom error classes

When to use AppError:

  • Creating domain-specific error classes (ValidationError, DatabaseError, etc.)
  • You need custom properties and behavior
  • Building a structured error hierarchy
  • You want full control over error naming and structure

Example:

import { sig } from '@simplicate/sigil';

// Custom error extending sig.AppError
class ValidationError extends sig.AppError {
  constructor(message: string, public field?: string) {
    super('ValidationError', message);
  }
}

class DatabaseError extends sig.AppError {
  constructor(message: string, public query?: string, public connectionString?: string) {
    super('DatabaseError', message);
  }
}

// Usage
const error = new ValidationError('Email format is invalid', 'email');
console.log(error.name); // 'ValidationError'
console.log(error.message); // 'Email format is invalid'
console.log(error.field); // 'email'

// JSON serialization
const serialized = error.toJSON();
console.log(JSON.stringify(serialized));

GenericAppError (Concrete Class)

A ready-to-use concrete error class for general error scenarios that don't require custom error classes.

constructor(message?: string, details?: Record<string, unknown>)
  • message: Optional error message (defaults to "An application error occurred")
  • details: Optional object containing additional error context

Features:

  • Fixed name: Always has the name 'GenericAppError'
  • Default message: Uses 'An application error occurred' if no message provided
  • Details property: Includes an optional details object for additional context
  • Extends AppError: Inherits all AppError benefits (stack traces, JSON serialization)

When to use GenericAppError:

  • Quick, one-off errors without creating a custom class
  • Handling unexpected/catch-all scenarios
  • You want to include additional context via the details property
  • Prototyping or when you don't want to create a specific error class yet
  • Simple error cases that don't warrant a custom error type

Examples:

import { sig } from '@simplicate/sigil';

// With custom message and details
throw new sig.GenericAppError('Something went wrong during processing', {
  userId: 123,
  operation: 'delete',
  timestamp: new Date().toISOString()
});

// With just a message
throw new sig.GenericAppError('Custom error message');

// With default message
throw new sig.GenericAppError();

// Accessing properties
try {
  throw new sig.GenericAppError('Process failed', { step: 'validation', code: 'INVALID_INPUT' });
} catch (error) {
  if (error instanceof sig.GenericAppError) {
    console.log(error.name);      // 'GenericAppError'
    console.log(error.message);   // 'Process failed'
    console.log(error.details);   // { step: 'validation', code: 'INVALID_INPUT' }
  }
}

Choosing Between AppError and GenericAppError:

| Use Case | AppError | GenericAppError | |----------|----------|----------------| | Domain-specific errors | ✅ Recommended | ❌ Not suitable | | Custom properties/methods | ✅ Required | ❌ Limited to details | | Quick prototyping | ❌ More setup | ✅ Recommended | | One-off errors | ❌ Overkill | ✅ Perfect | | Structured error hierarchy | ✅ Essential | ❌ Flat structure | | Additional context needed | ✅ Custom props | ✅ Details object | | Catch-all scenarios | ❌ Too specific | ✅ Ideal |

ErrorConfig

Configuration object for error processing:

type ErrorConfig = {
  code: string;           // Application-specific error code
  message: string;        // Human-readable error message
  severity: SeverityLevel; // Error severity level
}

Structured Logging

The error strategies automatically enrich log entries with structured context including:

  • Error codes and types for easy filtering and monitoring
  • Severity levels for appropriate log level selection
  • Original error objects and user-provided context
// Example structured log output:
// {"level":"warn","err":{...},"errorCode":"VALIDATION_ERROR","errorType":"ValidationError","severity":"medium","userId":123,"msg":"Input validation failed"}

ErrorResult

Structured result returned by error processing:

interface ErrorResult {
  code: string;                              // Application-specific error code
  message: string;                           // Human-readable error message
  severity: SeverityLevel;                   // Error severity level
  originalError: unknown;                    // The original error that was processed
  context?: Record<string, unknown>;         // Additional context or metadata
}

SeverityLevel

Error severity levels:

type SeverityLevel = 'low' | 'medium' | 'high' | 'critical';

Testing Utilities

Sigil provides comprehensive testing utilities that make writing maintainable, data-driven tests easier. The utilities are framework-agnostic and work with Jest, Vitest, Mocha, and other testing frameworks.

import { createErrorConfig, createTestContext, runTableTests, Utils } from '@simplicate/sigil/testing';

Key Features

  • 🎯 Table-Driven Tests: Write data-driven tests with minimal boilerplate
  • 🏭 Test Data Factories: Consistent, reusable test data creation
  • 🔄 Framework Agnostic: Works with any testing framework (Jest, Vitest, Mocha, etc.)
  • 📝 TypeScript First: Full type safety and IntelliSense support
  • 🧹 DRY Principle: Reduce code duplication and improve maintainability

Quick Comparison: Testing Utilities vs Direct Framework Usage

Traditional Jest/Vitest Approach (Verbose & Repetitive)

describe('Email Validation', () => {
  it('should accept valid email addresses', () => {
    expect(validateEmail('[email protected]')).toBe(true);
  });
  
  it('should reject email without @', () => {
    expect(validateEmail('userexample.com')).toBe(false);
  });
  
  it('should reject email without domain', () => {
    expect(validateEmail('user@')).toBe(false);
  });
  
  it('should reject empty email', () => {
    expect(validateEmail('')).toBe(false);
  });
  
  // ... 20 more similar tests with repetitive structure
});

Problems:

  • 🚫 Repetitive boilerplate code
  • 🚫 Hard to maintain when logic changes
  • 🚫 Test data scattered throughout
  • 🚫 Verbose for simple scenarios

With Sigil Testing Utilities (Clean & Maintainable)

import { runTableTests } from '@simplicate/sigil/testing';

describe('Email Validation', () => {
  interface EmailTestCase {
    name: string;
    email: string;
    expected: boolean;
  }

  const emailTestCases: EmailTestCase[] = [
    { name: 'should accept valid email addresses', email: '[email protected]', expected: true },
    { name: 'should reject email without @', email: 'userexample.com', expected: false },
    { name: 'should reject email without domain', email: 'user@', expected: false },
    { name: 'should reject empty email', email: '', expected: false },
    // ... 20 more cases - just data, no duplication
  ];

  runTableTests(emailTestCases, ({ name, email, expected }) => {
    it(name, () => {
      expect(validateEmail(email)).toBe(expected);
    });
  });
});

Benefits:

  • DRY: Single test function, multiple data cases
  • Maintainable: Easy to add/modify test cases
  • Data-driven: Clear separation of test data and logic
  • Scalable: Add more test cases without code changes

Testing Utility Functions

1. createErrorConfig() - Error Configuration Factory

Creates consistent error configurations with sensible defaults.

import { createErrorConfig } from '@simplicate/sigil/testing';

// Basic usage - uses defaults
const basicConfig = createErrorConfig();
// Result: { code: 'TEST_ERROR', message: 'Test error message', severity: 'medium' }

// With overrides
const customConfig = createErrorConfig({
  code: 'VALIDATION_ERROR',
  severity: 'high'
});
// Result: { code: 'VALIDATION_ERROR', message: 'Test error message', severity: 'high' }

Use Cases:

  • Testing error handler strategies
  • Consistent error configurations across tests
  • Mock error data creation

2. createTestContext() - Context Factory

Creates consistent context objects with standard test constants.

import { createTestContext } from '@simplicate/sigil/testing';

// Basic usage - uses test constants
const basicContext = createTestContext();
// Result: { userId: 12345, requestId: 'req-test-123', operation: 'test-operation', timestamp: Date }

// With overrides
const customContext = createTestContext({
  userId: 999,
  operation: 'user-registration',
  customField: 'test-value'
});
// Result: { userId: 999, requestId: 'req-test-123', operation: 'user-registration', timestamp: Date, customField: 'test-value' }

Use Cases:

  • API request context simulation
  • Error handler context testing
  • Consistent logging context

3. runTableTests() - Table-Driven Testing

Executes the same test logic across multiple data scenarios.

import { runTableTests } from '@simplicate/sigil/testing';

interface TestCase {
  name: string;
  input: any;
  expected: any;
}

const testCases: TestCase[] = [
  { name: 'scenario 1', input: 'data1', expected: 'result1' },
  { name: 'scenario 2', input: 'data2', expected: 'result2' },
  // ... more scenarios
];

runTableTests(testCases, ({ name, input, expected }) => {
  it(name, () => {
    const result = processData(input);
    expect(result).toBe(expected);
  });
});

Use Cases:

  • Input validation testing
  • Error scenario testing
  • API response testing
  • Any scenario with multiple similar test cases

4. Utils - Organized Namespace

Access all utilities through a organized namespace.

import { Utils } from '@simplicate/sigil/testing';

const errorConfig = Utils.createErrorConfig({ code: 'CUSTOM_ERROR' });
const context = Utils.createTestContext({ operation: 'test' });
Utils.runTableTests(testCases, testFn);

Real-World Examples

Error Handler Strategy Testing

import { createErrorConfig, createTestContext, runTableTests } from '@simplicate/sigil/testing';
import { sig } from '@simplicate/sigil';

describe('Error Handler Strategies', () => {
  interface ErrorStrategyCase {
    name: string;
    error: Error;
    expectedCode: string;
    expectedSeverity: 'low' | 'medium' | 'high' | 'critical';
  }

  const strategyCases: ErrorStrategyCase[] = [
    {
      name: 'should handle validation errors',
      error: new ValidationError('Invalid email'),
      expectedCode: 'VALIDATION_ERROR',
      expectedSeverity: 'medium'
    },
    {
      name: 'should handle database errors',
      error: new DatabaseError('Connection failed'),
      expectedCode: 'DATABASE_ERROR',
      expectedSeverity: 'high'
    },
    {
      name: 'should handle unknown errors',
      error: new Error('Unknown error'),
      expectedCode: 'UNKNOWN_ERROR',
      expectedSeverity: 'high'
    }
  ];

  runTableTests(strategyCases, ({ name, error, expectedCode, expectedSeverity }) => {
    it(name, () => {
      const validationStrategy = new sig.ConfigurableErrorStrategy(ValidationError, 
        createErrorConfig({ code: 'VALIDATION_ERROR', severity: 'medium' })
      );
      const dbStrategy = new sig.ConfigurableErrorStrategy(DatabaseError,
        createErrorConfig({ code: 'DATABASE_ERROR', severity: 'high' })
      );
      
      const errorHandler = new sig.GenericErrorHandler([validationStrategy, dbStrategy]);
      const context = createTestContext({ operation: 'strategy-test' });
      const mockLogger = jest.fn() as any;
      
      const result = errorHandler.handle(error, mockLogger, context);
      
      expect(result.code).toBe(expectedCode);
      expect(result.severity).toBe(expectedSeverity);
    });
  });
});

API Integration Testing

import { createTestContext, runTableTests } from '@simplicate/sigil/testing';

describe('API Error Handling', () => {
  interface APITestCase {
    name: string;
    statusCode: number;
    responseBody: any;
    expectedErrorCode: string;
    expectedSeverity: string;
  }

  const apiErrorCases: APITestCase[] = [
    {
      name: 'should handle 400 Bad Request',
      statusCode: 400,
      responseBody: { error: 'Invalid input' },
      expectedErrorCode: 'VALIDATION_ERROR',
      expectedSeverity: 'medium'
    },
    {
      name: 'should handle 401 Unauthorized',
      statusCode: 401,
      responseBody: { error: 'Token expired' },
      expectedErrorCode: 'AUTH_ERROR',
      expectedSeverity: 'medium'
    },
    {
      name: 'should handle 500 Internal Server Error',
      statusCode: 500,
      responseBody: { error: 'Database connection failed' },
      expectedErrorCode: 'SERVER_ERROR',
      expectedSeverity: 'high'
    }
  ];

  runTableTests(apiErrorCases, ({ name, statusCode, responseBody, expectedErrorCode, expectedSeverity }) => {
    it(name, async () => {
      const context = createTestContext({
        operation: 'api-request',
        endpoint: '/api/users'
      });

      const mockResponse = {
        status: statusCode,
        data: responseBody
      };

      const result = await apiErrorHandler.handleResponse(mockResponse, context);
      
      expect(result.code).toBe(expectedErrorCode);
      expect(result.severity).toBe(expectedSeverity);
      expect(result.context).toMatchObject(context);
    });
  });
});

Hybrid Approach: Best of Both Worlds

The most effective testing strategy combines Sigil utilities with framework-specific features:

import { createErrorConfig, createTestContext, runTableTests } from '@simplicate/sigil/testing';
import { sig } from '@simplicate/sigil';

describe('Payment Processing System', () => {
  
  // ✅ Use utilities for data-driven scenarios
  const paymentValidationCases = [
    { name: 'should accept valid amounts', amount: 100, valid: true },
    { name: 'should reject negative amounts', amount: -50, valid: false },
    { name: 'should reject zero amounts', amount: 0, valid: false },
    { name: 'should reject amounts over limit', amount: 100000, valid: false }
  ];

  runTableTests(paymentValidationCases, ({ name, amount, valid }) => {
    it(name, () => {
      expect(PaymentValidator.isValid(amount)).toBe(valid);
    });
  });

  // ✅ Use direct Jest/Vitest for complex scenarios
  it('should handle Stripe webhook integration with proper mocking', async () => {
    // Complex setup that benefits from framework-specific features
    const mockStripeWebhook = jest.fn().mockResolvedValue({
      id: 'evt_123',
      type: 'payment_intent.succeeded',
      data: { object: { amount: 2000 } }
    });
    
    const context = createTestContext({ 
      operation: 'webhook-processing',
      source: 'stripe'
    });
    
    // Use Jest-specific mocking capabilities
    jest.spyOn(StripeClient.prototype, 'webhooks').mockImplementation(() => ({
      constructEvent: mockStripeWebhook
    }));
    
    const webhook = new StripeWebhookHandler();
    webhook.onPaymentSuccess = jest.fn();
    
    await webhook.process(mockWebhookEvent, context);
    
    expect(webhook.onPaymentSuccess).toHaveBeenCalledWith(
      expect.objectContaining({ amount: 2000 })
    );
    expect(mockStripeWebhook).toHaveBeenCalledTimes(1);
  });

  // ✅ Use utilities for error scenario testing
  const errorScenarios = [
    {
      name: 'should handle payment declined',
      error: new PaymentDeclinedError('Card declined'),
      expectedCode: 'PAYMENT_DECLINED',
      expectedSeverity: 'medium' as const
    },
    {
      name: 'should handle network timeout',
      error: new NetworkTimeoutError('Request timeout'),
      expectedCode: 'NETWORK_ERROR',
      expectedSeverity: 'high' as const
    }
  ];

  runTableTests(errorScenarios, ({ name, error, expectedCode, expectedSeverity }) => {
    it(name, () => {
      const errorConfig = createErrorConfig({ code: expectedCode, severity: expectedSeverity });
      const context = createTestContext({ operation: 'payment-processing' });
      
      const result = paymentErrorHandler.handle(error, mockLogger, context);
      
      expect(result.code).toBe(expectedCode);
      expect(result.severity).toBe(expectedSeverity);
    });
  });
});

When to Use Each Approach

Use Sigil Testing Utilities When:

  • Table-driven tests with multiple similar scenarios
  • Consistent test data needed across tests
  • Framework independence desired for portability
  • Large test suites that need maintainability
  • Team consistency is important
  • Data-driven testing patterns

Use Jest/Vitest Direct When:

  • Complex mocking requirements (spies, stubs, module mocks)
  • Framework-specific features needed (snapshots, timers)
  • One-off tests with unique logic
  • Integration with framework ecosystems
  • Learning/prototyping where direct API is clearer

Use Hybrid Approach When:

  • Real-world applications (most common scenario)
  • Team has mixed experience levels
  • Balancing maintainability with flexibility
  • Different test types in the same suite

Framework Compatibility

The testing utilities work seamlessly across testing frameworks:

// Jest
runTableTests(testCases, ({ name, input, expected }) => {
  it(name, () => {
    expect(processor(input)).toBe(expected);
  });
});

// Vitest  
runTableTests(testCases, ({ name, input, expected }) => {
  test(name, () => {
    expect(processor(input)).toBe(expected);
  });
});

// Mocha + Chai
runTableTests(testCases, ({ name, input, expected }) => {
  it(name, () => {
    assert.equal(processor(input), expected);
  });
});

Migration Benefits

Migrating from direct framework usage to Sigil utilities provides:

  • 📊 60-80% reduction in test code volume
  • 🎯 Improved maintainability - changes in one place
  • 🔄 Framework flexibility - easy to switch testing frameworks
  • 📝 Better test documentation - data tables are self-documenting
  • 🧹 DRY compliance - no more repetitive test functions
  • ⚡ Faster test writing - focus on data, not boilerplate

The utilities complement rather than replace Jest/Vitest, giving you the best of both worlds for comprehensive testing strategies.

Project Structure

The library is organized with a clean, flat structure for easy navigation and maintenance:

src/
├── index.ts                    # Main barrel export
├── app-error.ts               # AppError base class
├── error-handler.ts           # GenericErrorHandler class
├── error-types.ts             # Types and interfaces
├── logger.ts                  # Logger interface and types
├── strategies/                # Error handling strategies
│   ├── index.ts
│   ├── configurable-strategy.ts
│   └── default-strategy.ts
└── testing/                   # Testing utilities (separate export)
    ├── index.ts
    ├── error-test-utils.ts
    ├── types.ts
    └── adapters/              # Test framework adapters
        ├── adapter-factory.ts
        ├── jest-adapter.ts
        └── vitest-adapter.ts

Design Principles

  • Flat Structure: Core files at the root level for easy access
  • Logical Grouping: Related functionality properly namespaced (strategies, testing)
  • Path Mappings: TypeScript path mappings for clean imports (@strategies/*, @testing/*)
  • Separate Exports: Testing utilities available as @simplicate/sigil/testing
  • Framework Agnostic: No assumptions about your application framework or testing setup

Logger Interface

The package expects a logger that implements the AppLogger interface:

interface AppLogger {
  child(bindings: Record<string, unknown>): AppLogger;
  debug(obj: Record<string, unknown>, message?: string): void;
  debug(message: string): void;
  error(obj: Record<string, unknown>, message?: string): void;
  error(message: string): void;
  info(obj: Record<string, unknown>, message?: string): void;
  info(message: string): void;
  warn(obj: Record<string, unknown>, message?: string): void;
  warn(message: string): void;
}

Compatible with popular loggers:

  • Pino: Works directly
  • Winston: Works with structured logging format
  • Bunyan: Compatible interface
  • Custom: Implement the interface for any logger

Example with different loggers:

// Pino (direct compatibility)
import pino from 'pino';
const logger = pino();

// Winston (with adapter if needed)
import winston from 'winston';
const winstonLogger = winston.createLogger({ /* config */ });
// Use directly or create simple adapter

// Custom logger
const customLogger: AppLogger = {
  child: (bindings) => ({ ...customLogger, bindings }),
  debug: (obj, msg) => console.debug(msg, obj),
  error: (obj, msg) => console.error(msg, obj),
  info: (obj, msg) => console.info(msg, obj),
  warn: (obj, msg) => console.warn(msg, obj),
};

License

ISC