diff --git a/.changeset/light-masks-pick.md b/.changeset/light-masks-pick.md new file mode 100644 index 0000000..c94ae1f --- /dev/null +++ b/.changeset/light-masks-pick.md @@ -0,0 +1,5 @@ +--- +'@power-rent/try-catch': patch +--- + +Use type-fest to fix breadcrumbs type definition diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebd4f6c..81b5bdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: - node-version: "20.x" - registry-url: "https://registry.npmjs.org" - cache: "npm" + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + cache: 'npm' - name: Install dependencies run: npm ci diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d24fdfc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c41de3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,447 @@ +# Contributing to @power-rent/try-catch + +Thank you for your interest in contributing to the try-catch library! This guide will help you understand the development workflow, including how to make changes, manage versions, and create changelogs. + +## Table of Contents + +- [Development Setup](#development-setup) +- [Making Changes](#making-changes) +- [Changelog Creation Process](#changelog-creation-process) +- [Release Process](#release-process) +- [Version Management](#version-management) +- [Testing](#testing) +- [Code Style](#code-style) +- [Pull Request Process](#pull-request-process) + +## Development Setup + +### Prerequisites + +- Node.js >= 20 +- npm (comes with Node.js) +- Git + +### Installation + +```bash +# Clone the repository +git clone https://github.com/Toprent-app/try-catch.git +cd try-catch + +# Install dependencies +npm install + +# Build the project +npm run build + +# Run tests +npm test +``` + +### Available Scripts + +```bash +npm run build # Build both CommonJS and ESM versions +npm run build:cjs # Build CommonJS version only +npm run build:esm # Build ESM version only +npm run test # Run tests once +npm run test:watch # Run tests in watch mode +npm run format # Format code with Prettier +npm run format:check # Check code formatting +npm run typecheck # Run TypeScript type checking +npm run clean # Clean build artifacts +``` + +## Making Changes + +### 1. Create a Feature Branch + +```bash +git checkout -b feature/your-feature-name +``` + +### 2. Make Your Changes + +- Write your code following the existing patterns +- Add tests for new functionality +- Update documentation if needed +- Ensure all tests pass: `npm test` + +### 3. Create a Changeset + +This project uses [Changesets](https://github.com/changesets/changesets) for version management and changelog generation. + +#### What is a Changeset? + +A changeset is a file that describes what changes you've made and what type of version bump they require. This information is used to: +- Automatically generate changelogs +- Determine version bumps (major, minor, patch) +- Create release pull requests + +#### Creating a Changeset + +1. **Run the changeset command:** + ```bash + npx changeset + ``` + +2. **Follow the interactive prompts:** + - Select which packages to include (this project has one package: `@power-rent/try-catch`) + - Choose the version bump type: + - **Major**: Breaking changes that require users to update their code + - **Minor**: New features that are backward compatible + - **Patch**: Bug fixes that are backward compatible + - Write a description of your changes + +3. **The command will create a file** in the `.changeset/` directory with a random name like `cool-cats-sing.md` + +#### Manual Changeset Creation + +You can also create changeset files manually: + +1. **Create a new file** in the `.changeset/` directory with a descriptive name: + ```bash + touch .changeset/your-descriptive-name.md + ``` + +2. **Add the changeset content:** + ```markdown + --- + '@power-rent/try-catch': minor + --- + + Add new breadcrumb extraction feature for better Sentry integration + ``` + +#### Changeset File Format + +```markdown +--- +'@power-rent/try-catch': major|minor|patch +--- + +Description of your changes. This will appear in the changelog. +``` + +**Examples:** + +```markdown +--- +'@power-rent/try-catch': major +--- + +BREAKING: Remove deprecated error reporter class and update API +``` + +```markdown +--- +'@power-rent/try-catch': minor +--- + +Add support for custom breadcrumb transformers +``` + +```markdown +--- +'@power-rent/try-catch': patch +--- + +Fix memory leak in error reporting +``` + +### 4. Commit Your Changes + +```bash +git add . +git commit -m "feat: add new breadcrumb extraction feature + +- Add support for custom transformers +- Improve TypeScript types +- Add comprehensive tests" +``` + +**Commit Message Format:** +- Use conventional commits format: `type(scope): description` +- Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` +- Include the changeset file in your commit + +### 5. Push and Create Pull Request + +```bash +git push origin feature/your-feature-name +``` + +Then create a pull request on GitHub. + +## Changelog Creation Process + +The changelog is automatically generated using Changesets. Here's how it works: + +### Automatic Process + +1. **Changeset Creation**: When you create a changeset file, it describes your changes +2. **Release PR Generation**: GitHub Actions automatically creates a "Version Packages" PR when changesets exist +3. **Changelog Generation**: When the release PR is merged, the changelog is automatically updated +4. **Package Publishing**: The package is automatically published to npm + +### Manual Changelog Preview + +You can preview what the changelog will look like: + +```bash +# See what changesets exist +npx changeset status + +# Preview the release plan +npx changeset version --dry-run +``` + +### Changelog Structure + +The generated `CHANGELOG.md` follows this format: + +```markdown +# @power-rent/try-catch + +## 1.1.0 + +### Minor Changes + +- a1b2c3d: Add support for custom breadcrumb transformers + +## 1.0.0 + +### Major Changes + +- e4f5g6h: BREAKING: Remove deprecated error reporter class + +### Minor Changes + +- i7j8k9l: Improved developer experience and documentation +``` + +## Release Process + +### Automated Release via GitHub Actions + +The project uses GitHub Actions for automated releases: + +1. **Trigger**: Push to `main` branch +2. **Workflow**: `.github/workflows/release.yml` +3. **Action**: `changesets/action@v1` + +#### What Happens: + +1. **Build**: Installs dependencies and builds the project +2. **Check for Changesets**: Looks for changeset files +3. **Create Release PR**: If changesets exist, creates a "Version Packages" PR +4. **Publish**: If release PR is merged, publishes to npm and updates changelog + +#### Release PR Process: + +1. GitHub Actions creates a PR titled "Version Packages" +2. The PR includes: + - Updated `package.json` version + - Updated `CHANGELOG.md` + - Removed changeset files +3. Review and merge the PR +4. The package is automatically published to npm + +### Manual Release (if needed) + +```bash +# Create release PR manually +npx changeset version + +# Publish (requires npm login) +npx changeset publish +``` + +## Version Management + +### Semantic Versioning + +This project follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** (1.0.0 → 2.0.0): Breaking changes +- **MINOR** (1.0.0 → 1.1.0): New features, backward compatible +- **PATCH** (1.0.0 → 1.0.1): Bug fixes, backward compatible + +### Version Bump Guidelines + +**Major (Breaking Changes):** +- Removing public APIs +- Changing function signatures +- Changing behavior in a way that breaks existing code +- Removing deprecated features + +**Minor (New Features):** +- Adding new methods or properties +- Adding new functionality +- Improving existing features without breaking changes + +**Patch (Bug Fixes):** +- Fixing bugs +- Improving performance +- Updating documentation +- Internal refactoring + +## Testing + +### Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test -- --coverage +``` + +### Writing Tests + +- Tests are located in `src/__tests__/` +- Use Vitest as the testing framework +- Follow the existing test patterns +- Test both success and error cases +- Include edge cases and type safety tests + +### Test Structure + +```typescript +import { describe, it, expect } from 'vitest'; +import { Try } from '../Try'; + +describe('Try class', () => { + it('should handle successful operations', async () => { + const result = await new Try(() => 'success').value(); + expect(result).toBe('success'); + }); + + it('should handle errors gracefully', async () => { + const error = await new Try(() => { + throw new Error('test error'); + }).error(); + + expect(error).toBeInstanceOf(Error); + expect(error?.message).toBe('test error'); + }); +}); +``` + +## Code Style + +### Formatting + +This project uses Prettier for code formatting: + +```bash +# Format all files +npm run format + +# Check formatting +npm run format:check +``` + +### TypeScript + +- Use strict TypeScript settings +- Provide proper type annotations +- Use generic types where appropriate +- Follow the existing type patterns + +### Code Organization + +- Keep functions small and focused +- Use descriptive names +- Add JSDoc comments for public APIs +- Follow the existing file structure + +## Pull Request Process + +### Before Submitting + +1. **Ensure tests pass**: `npm test` +2. **Check formatting**: `npm run format:check` +3. **Type check**: `npm run typecheck` +4. **Build successfully**: `npm run build` +5. **Include changeset**: Make sure you've created a changeset file + +### PR Description Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update + +## Changeset +- [ ] I have created a changeset for this PR + +## Testing +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] All tests pass locally + +## Checklist +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +``` + +### Review Process + +1. **Automated Checks**: CI will run tests, type checking, and formatting +2. **Code Review**: Maintainers will review your code +3. **Changeset Review**: Ensure the changeset accurately describes your changes +4. **Merge**: Once approved, the PR will be merged +5. **Release**: GitHub Actions will create a release PR if changesets exist + +## Getting Help + +- **Issues**: Create an issue for bugs or feature requests +- **Discussions**: Use GitHub Discussions for questions +- **Documentation**: Check the README and examples for usage patterns + +## Development Tips + +### Local Development + +```bash +# Watch mode for development +npm run test:watch + +# Build in watch mode (if using a build tool that supports it) +npm run build -- --watch +``` + +### Debugging + +```bash +# Enable debug logging in your code +const result = await new Try(riskyFunction, params) + .debug(true) // Enable debug logging + .report('Function failed') + .value(); +``` + +### Performance Testing + +```bash +# Run performance tests +npm test -- --reporter=verbose +``` + +## Release History + +The project uses automated releases. Check the [CHANGELOG.md](./CHANGELOG.md) for a complete history of changes. + +--- + +Thank you for contributing to @power-rent/try-catch! šŸš€ diff --git a/examples/comprehensive-examples.ts b/examples/comprehensive-examples.ts index 7942b81..5e0e634 100644 --- a/examples/comprehensive-examples.ts +++ b/examples/comprehensive-examples.ts @@ -43,7 +43,7 @@ interface UpdateOptions { // Mock async functions that might fail async function fetchUser(id: string): Promise { - await new Promise(resolve => setTimeout(resolve, 100)); // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 100)); // Simulate network delay if (id === 'invalid') { throw new Error(`User not found: ${id}`); @@ -53,12 +53,15 @@ async function fetchUser(id: string): Promise { id, name: `User ${id}`, email: `user${id}@example.com`, - age: Math.floor(Math.random() * 50) + 20 + age: Math.floor(Math.random() * 50) + 20, }; } -async function updateUser(userData: UserData, options: UpdateOptions = {}): Promise { - await new Promise(resolve => setTimeout(resolve, 50)); +async function updateUser( + userData: UserData, + options: UpdateOptions = {}, +): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); if (!userData.email.includes('@')) { throw new Error('Invalid email format'); @@ -70,7 +73,7 @@ async function updateUser(userData: UserData, options: UpdateOptions = {}): Prom return { id: 'updated-user', - ...userData + ...userData, }; } @@ -101,14 +104,20 @@ async function demonstrateBasicUsage() { // 2. Function with multiple parameter types console.log('\n2. Multiple parameter types:'); - const message = await new Try(formatMessage, 42, 'System ready', true).value(); + const message = await new Try( + formatMessage, + 42, + 'System ready', + true, + ).value(); console.log('āœ… Formatted message:', message); // 3. Function with object parameters console.log('\n3. Object parameters:'); - const updatedUser = await new Try(updateUser, + const updatedUser = await new Try( + updateUser, { name: 'John Doe', email: 'john@example.com' }, - { validateOnly: false } + { validateOnly: false }, ).value(); console.log('āœ… Updated user:', updatedUser?.name); @@ -155,7 +164,11 @@ async function demonstrateErrorStrategies() { // Strategy 5: Default values console.log('\n5. Default Values:'); const userWithDefault = await new Try(fetchUser, 'invalid') - .default({ id: 'default', name: 'Default User', email: 'default@example.com' }) + .default({ + id: 'default', + name: 'Default User', + email: 'default@example.com', + }) .value(); console.log('āœ… User with default:', userWithDefault?.name); } @@ -174,7 +187,7 @@ async function demonstrateBreadcrumbPatterns() { message: config.message, error: error.message, breadcrumbs: config.breadcrumbData, - tags: config.tags + tags: config.tags, }); } @@ -190,9 +203,10 @@ async function demonstrateBreadcrumbPatterns() { Try.setDefaultReporter(new TestReporter()); console.log('\n1. Simple key extraction:'); - await new Try(updateUser, + await new Try( + updateUser, { name: 'John', email: 'john@example.com', age: 30 }, - { validateOnly: true } + { validateOnly: true }, ) .breadcrumbs(['name', 'email']) .report('User update failed') @@ -201,36 +215,44 @@ async function demonstrateBreadcrumbPatterns() { console.log('\n2. Transformer function breadcrumb extraction:'); await new Try(formatMessage, 999, 'Test message', true) .breadcrumbs( - (id: number) => ({ messageId: id }), // Transform first param - (msg: string) => ({ content: msg }), // Transform second param - (urgent: boolean) => ({ isUrgent: urgent }) // Transform third param + (id: number) => ({ messageId: id }), // Transform first param + (msg: string) => ({ content: msg }), // Transform second param + (urgent: boolean) => ({ isUrgent: urgent }), // Transform third param ) .report('Message formatting failed') .value(); console.log('\n3. Object syntax with transformers:'); - await new Try(updateUser, + await new Try( + updateUser, { name: 'Jane', email: 'invalid-email', age: 25 }, - { validateOnly: false, skipNotification: true } + { validateOnly: false, skipNotification: true }, ) .breadcrumbs({ - 0: ['name', 'age'], // Extract keys from first parameter - 1: (opts: any) => ({ hasValidation: !!opts.validateOnly, optionCount: Object.keys(opts).length }) + 0: ['name', 'age'], // Extract keys from first parameter + 1: (opts: any) => ({ + hasValidation: !!opts.validateOnly, + optionCount: Object.keys(opts).length, + }), }) .report('Complex update failed') .value(); console.log('\n4. Mixed extraction strategies using extractor objects:'); - async function processOrder(orderId: string, customerData: { id: number, type: string }, urgent: boolean) { + async function processOrder( + orderId: string, + customerData: { id: number; type: string }, + urgent: boolean, + ) { if (orderId === 'invalid') throw new Error('Invalid order'); return { orderId, processed: true }; } await new Try(processOrder, 'invalid', { id: 123, type: 'premium' }, true) .breadcrumbs([ - { param: 0, as: 'value' }, // Extract orderId as value - { param: 1, keys: ['id', 'type'] }, // Extract keys from customerData - { param: 2, as: 'value' } // Extract urgent as value + { param: 0, as: 'value' }, // Extract orderId as value + { param: 1, keys: ['id', 'type'] }, // Extract keys from customerData + { param: 2, as: 'value' }, // Extract urgent as value ]) .report('Order processing failed') .value(); @@ -267,7 +289,10 @@ async function demonstratePlatformSpecific() { console.log('āœ… Config loaded:', config); console.log('\n2. Browser-style API calls:'); - async function fetchFromAPI(endpoint: string, options: RequestInit = {}): Promise { + async function fetchFromAPI( + endpoint: string, + options: RequestInit = {}, + ): Promise { if (endpoint.includes('error')) { throw new Error('Network request failed'); } @@ -279,7 +304,10 @@ async function demonstratePlatformSpecific() { .tag('component', 'api-client') .breadcrumbs([ { param: 0, as: 'value' }, - { param: 1, transform: (opts: any) => ({ method: opts.method || 'GET' }) } + { + param: 1, + transform: (opts: any) => ({ method: opts.method || 'GET' }), + }, ]) .default(null) .value(); @@ -313,42 +341,70 @@ class ApiClient { this.baseUrl = baseUrl; } - async get(endpoint: string, headers: Record = {}): Promise { + async get( + endpoint: string, + headers: Record = {}, + ): Promise { return this.makeRequest('GET', endpoint, undefined, headers); } - async post(endpoint: string, body: any, headers: Record = {}): Promise { + async post( + endpoint: string, + body: any, + headers: Record = {}, + ): Promise { return this.makeRequest('POST', endpoint, body, headers); } - async put(endpoint: string, body: any, headers: Record = {}): Promise { + async put( + endpoint: string, + body: any, + headers: Record = {}, + ): Promise { return this.makeRequest('PUT', endpoint, body, headers); } - async delete(endpoint: string, headers: Record = {}): Promise { + async delete( + endpoint: string, + headers: Record = {}, + ): Promise { return this.makeRequest('DELETE', endpoint, undefined, headers); } - private async makeRequest(method: string, endpoint: string, body?: any, headers: Record = {}): Promise { + private async makeRequest( + method: string, + endpoint: string, + body?: any, + headers: Record = {}, + ): Promise { // Simulate API call - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); if (endpoint.includes('error')) { - throw new Error(`HTTP ${method} request failed for ${this.baseUrl}${endpoint}`); + throw new Error( + `HTTP ${method} request failed for ${this.baseUrl}${endpoint}`, + ); } return { data: `Response from ${method} ${this.baseUrl}${endpoint}`, body, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; } } class UserRepository { private apiClient: ApiClient; - private boundGet: (endpoint: string, headers?: Record) => Promise; - private boundPost: (endpoint: string, body: any, headers?: Record) => Promise; + private boundGet: ( + endpoint: string, + headers?: Record, + ) => Promise; + private boundPost: ( + endpoint: string, + body: any, + headers?: Record, + ) => Promise; constructor(apiClient: ApiClient) { this.apiClient = apiClient; @@ -373,7 +429,7 @@ class UserRepository { async findByIdWithWrapper(id: string): Promise { const result = await new Try( (endpoint: string) => this.apiClient.get(endpoint), - `/users/${id}` + `/users/${id}`, ) .tag('repository', 'user') .tag('operation', 'findById') @@ -385,11 +441,11 @@ class UserRepository { // āœ… SOLUTION 2: Use .bind() method async findByIdWithBind(id: string): Promise { - const boundGet = this.apiClient.get.bind(this.apiClient) as (endpoint: string, headers?: Record) => Promise; - const result = await new Try( - boundGet, - `/users/${id}` - ) + const boundGet = this.apiClient.get.bind(this.apiClient) as ( + endpoint: string, + headers?: Record, + ) => Promise; + const result = await new Try(boundGet, `/users/${id}`) .tag('repository', 'user') .tag('operation', 'findById') .breadcrumbs([{ param: 0, as: 'value' }]) @@ -401,18 +457,24 @@ class UserRepository { // āœ… SOLUTION 3: Multi-parameter wrapper for complex cases async createUser(userData: CreateUserData): Promise { const result = await new Try( - (endpoint: string, body: CreateUserData, headers?: Record) => - this.apiClient.post(endpoint, body, headers), + ( + endpoint: string, + body: CreateUserData, + headers?: Record, + ) => this.apiClient.post(endpoint, body, headers), '/users', userData, - { 'Content-Type': 'application/json' } + { 'Content-Type': 'application/json' }, ) .tag('repository', 'user') .tag('operation', 'create') .breadcrumbs({ 0: (endpoint: any) => ({ endpoint }), 1: ['name', 'email'], - 2: (headers: any) => ({ hasHeaders: !!headers, headerCount: Object.keys(headers || {}).length }) + 2: (headers: any) => ({ + hasHeaders: !!headers, + headerCount: Object.keys(headers || {}).length, + }), }) .report('Failed to create user') .value(); @@ -420,22 +482,22 @@ class UserRepository { } // āœ… SOLUTION 4: Using bound methods stored as class properties (initialized in constructor) - async updateUserWithBoundMethods(id: string, updates: Partial): Promise { - const result = await new Try( - this.boundPost, - `/users/${id}`, - updates - ) + async updateUserWithBoundMethods( + id: string, + updates: Partial, + ): Promise { + const result = await new Try(this.boundPost, `/users/${id}`, updates) .tag('repository', 'user') .tag('operation', 'update') .breadcrumbs([ { param: 0, as: 'value' }, { - param: 1, transform: (updates: any) => ({ + param: 1, + transform: (updates: any) => ({ updateFields: Object.keys(updates), - fieldCount: Object.keys(updates).length - }) - } + fieldCount: Object.keys(updates).length, + }), + }, ]) .report(`Failed to update user ${id}`) .value(); @@ -455,7 +517,7 @@ class UserService { async findById(id: string): Promise { const result = await new Try( (userId: string) => this.userRepository.findByIdWithWrapper(userId), - id + id, ) .tag('service', 'user') .tag('layer', 'business-logic') @@ -473,7 +535,7 @@ class UserService { const result = await new Try( (data: CreateUserData) => this.userRepository.createUser(data), - userData + userData, ) .tag('service', 'user') .tag('layer', 'business-logic') @@ -503,26 +565,37 @@ async function demonstrateClassMethodBinding() { const newUser = await userRepository.createUser({ name: 'John Doe', email: 'john@example.com', - password: 'secure123' + password: 'secure123', }); console.log('āœ… User created with multi-param wrapper:', newUser); console.log('\n4. Testing bound methods:'); - const updatedUser = await userRepository.updateUserWithBoundMethods('user-789', { - name: 'Jane Smith', - age: 30 - }); + const updatedUser = await userRepository.updateUserWithBoundMethods( + 'user-789', + { + name: 'Jane Smith', + age: 30, + }, + ); console.log('āœ… User updated with bound methods:', updatedUser); console.log('\n5. Testing service layer delegation:'); const serviceUser = await userService.findById('user-service-test'); console.log('āœ… User found via service:', serviceUser); - console.log('\n6. Demonstrating the wrong approach (commented out to avoid errors):'); + console.log( + '\n6. Demonstrating the wrong approach (commented out to avoid errors):', + ); console.log('// āŒ This would fail:'); - console.log('// await new Try(this.apiClient.post, \'/users\', userData)'); - console.log('// Error: Cannot read properties of undefined (reading \'makeRequest\')'); - console.log('\nāœ… Key takeaway: Always wrap class methods to preserve \'this\' context!'); + console.log( + "// await new Try(this.apiClient.post, '/users', userData)", + ); + console.log( + "// Error: Cannot read properties of undefined (reading 'makeRequest')", + ); + console.log( + "\nāœ… Key takeaway: Always wrap class methods to preserve 'this' context!", + ); } // ============================================================================= @@ -538,27 +611,48 @@ class RealWorldApiClient { } // āœ… Proper implementation using private method with correct binding - async get(endpoint: string, headers: Record = {}): Promise { + async get( + endpoint: string, + headers: Record = {}, + ): Promise { const result = await new Try( - (method: string, ep: string, body: any, h: Record) => this.makeRequest(method, ep, body, h), - 'GET', endpoint, undefined, headers + (method: string, ep: string, body: any, h: Record) => + this.makeRequest(method, ep, body, h), + 'GET', + endpoint, + undefined, + headers, ) .tag('method', 'GET') .tag('client', 'api') .breadcrumbs([ - { param: 0, as: 'value' }, // httpMethod - { param: 1, as: 'value' }, // endpoint - { param: 3, transform: (h: any) => ({ headerCount: Object.keys(h).length, hasAuth: !!h.authorization }) } + { param: 0, as: 'value' }, // httpMethod + { param: 1, as: 'value' }, // endpoint + { + param: 3, + transform: (h: any) => ({ + headerCount: Object.keys(h).length, + hasAuth: !!h.authorization, + }), + }, ]) .report(`Failed to GET ${endpoint}`) .value(); return result; } - async post(endpoint: string, body: any, headers: Record = {}): Promise { + async post( + endpoint: string, + body: any, + headers: Record = {}, + ): Promise { const result = await new Try( - (method: string, ep: string, b: any, h: Record) => this.makeRequest(method, ep, b, h), - 'POST', endpoint, body, headers + (method: string, ep: string, b: any, h: Record) => + this.makeRequest(method, ep, b, h), + 'POST', + endpoint, + body, + headers, ) .tag('method', 'POST') .tag('client', 'api') @@ -566,16 +660,21 @@ class RealWorldApiClient { 0: (method: any) => ({ httpMethod: method }), 1: (endpoint: any) => ({ endpoint }), 2: (body: any) => ({ bodyType: typeof body, hasData: !!body }), - 3: (headers: any) => ({ headerCount: Object.keys(headers).length }) + 3: (headers: any) => ({ headerCount: Object.keys(headers).length }), }) .report(`Failed to POST ${endpoint}`) .value(); return result; } - private async makeRequest(method: string, endpoint: string, body?: any, headers: Record = {}): Promise { + private async makeRequest( + method: string, + endpoint: string, + body?: any, + headers: Record = {}, + ): Promise { // Simulate API call - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); if (endpoint.includes('error')) { throw new Error(`HTTP ${method} request failed`); @@ -583,7 +682,7 @@ class RealWorldApiClient { return { data: `Response from ${method} ${endpoint}`, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; } } @@ -598,8 +697,9 @@ class RealWorldUserService { // āœ… Proper implementation using wrapper function async findById(id: string): Promise { const result = await new Try( - (endpoint: string, headers?: Record) => this.apiClient.get(endpoint, headers), - `/users/${id}` + (endpoint: string, headers?: Record) => + this.apiClient.get(endpoint, headers), + `/users/${id}`, ) .tag('service', 'user') .tag('operation', 'findById') @@ -611,16 +711,19 @@ class RealWorldUserService { async create(userData: CreateUserData): Promise { const result = await new Try( - (endpoint: string, body: CreateUserData, headers?: Record) => - this.apiClient.post(endpoint, body, headers), + ( + endpoint: string, + body: CreateUserData, + headers?: Record, + ) => this.apiClient.post(endpoint, body, headers), '/users', - userData + userData, ) .tag('service', 'user') .tag('operation', 'create') .breadcrumbs({ 0: (endpoint: any) => ({ endpoint }), - 1: ['name', 'email'] + 1: ['name', 'email'], }) .report('Failed to create user') .value(); @@ -629,20 +732,22 @@ class RealWorldUserService { async update(id: string, updates: Partial): Promise { const result = await new Try( - (endpoint: string, body: Partial) => this.apiClient.post(endpoint, body), + (endpoint: string, body: Partial) => + this.apiClient.post(endpoint, body), `/users/${id}`, - updates + updates, ) .tag('service', 'user') .tag('operation', 'update') .breadcrumbs([ - { param: 0, as: 'value' }, // endpoint with userId + { param: 0, as: 'value' }, // endpoint with userId { - param: 1, transform: (updates: any) => ({ + param: 1, + transform: (updates: any) => ({ updateFields: Object.keys(updates), - fieldCount: Object.keys(updates).length - }) - } + fieldCount: Object.keys(updates).length, + }), + }, ]) .report(`Failed to update user ${id}`) .value(); @@ -668,14 +773,14 @@ async function demonstrateRealWorldPatterns() { const newUser = await userService.create({ name: 'Alice Johnson', email: 'alice@example.com', - password: 'secure123' + password: 'secure123', }); console.log('āœ… Created user:', newUser); console.log('\n4. Update operation with partial data:'); const updatedUser = await userService.update('user-456', { name: 'Alice Smith', - age: 28 + age: 28, }); console.log('āœ… Updated user:', updatedUser); } @@ -700,7 +805,7 @@ async function demonstrateAdvancedConfiguration() { console.log('\n2. Finally callbacks:'); let cleanupCalled = false; const resultWithCleanup = await new Try(async () => { - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); return 'Operation completed'; }) .finally(() => { @@ -750,7 +855,7 @@ async function demonstrateAdvancedConfiguration() { environment, version, component: 'user-fetcher', - correlationId: Math.random().toString(36) + correlationId: Math.random().toString(36), }) .tag('timestamp', new Date().toISOString()) .debug(environment === 'development') @@ -766,7 +871,10 @@ async function demonstrateAdvancedConfiguration() { class TestReporter implements Reporter { public reports: Array<{ error: Error; config: ErrorReportConfig }> = []; - public breadcrumbs: Array<{ data: Record; functionName?: string }> = []; + public breadcrumbs: Array<{ + data: Record; + functionName?: string; + }> = []; report(error: Error, config: ErrorReportConfig): void { this.reports.push({ error, config }); @@ -807,9 +915,10 @@ async function demonstrateTestingPatterns() { console.log('\n2. Testing breadcrumb extraction:'); testReporter.reset(); - await new Try(updateUser, + await new Try( + updateUser, { name: 'Test User', email: 'test@example.com' }, - { validateOnly: true } + { validateOnly: true }, ) .breadcrumbs(['name', 'email']) .report('Test breadcrumb extraction') @@ -854,7 +963,6 @@ async function runComprehensiveExamples() { console.log('\n' + '='.repeat(80)); console.log('āœ… ALL EXAMPLES COMPLETED SUCCESSFULLY'); console.log('='.repeat(80)); - } catch (error) { console.error('\nāŒ Example execution failed:', error); process.exit(1); @@ -866,9 +974,4 @@ if (require.main === module) { runComprehensiveExamples(); } -export { - runComprehensiveExamples, - ApiClient, - UserService, - TestReporter -}; +export { runComprehensiveExamples, ApiClient, UserService, TestReporter }; diff --git a/examples/custom-reporter.ts b/examples/custom-reporter.ts index 630734b..f96f1d3 100644 --- a/examples/custom-reporter.ts +++ b/examples/custom-reporter.ts @@ -40,7 +40,7 @@ async function demonstrateReporters() { const result1 = await new Try(() => { throw new Error('Test error 1'); }) - .report('This error won\'t be reported anywhere') + .report("This error won't be reported anywhere") .value(); console.log('Result 1 (NoopReporter):', result1); // undefined @@ -61,7 +61,7 @@ async function demonstrateReporters() { const result3 = await new Try(() => { return 'Success!'; }) - .report('This won\'t be called since there\'s no error') + .report("This won't be called since there's no error") .value(); console.log('Result 3 (Success):', result3); // "Success!" diff --git a/package-lock.json b/package-lock.json index 1b3bf3d..4896bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { "name": "@power-rent/try-catch", - "version": "0.0.7", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@power-rent/try-catch", - "version": "0.0.7", + "version": "1.0.0", "license": "ISC", "devDependencies": { "@changesets/cli": "^2.29.6", "@types/node": "^20.19.1", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", "prettier": "^3.6.2", + "type-fest": "^5.0.0", "typescript": "^5.9.2", "vitest": "^3.2.4" }, @@ -4012,6 +4015,22 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", + "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4354,6 +4373,39 @@ "optional": true, "peer": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4411,6 +4463,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -4534,6 +4593,13 @@ "optional": true, "peer": true }, + "node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -4563,6 +4629,19 @@ "node": ">=8.6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -4698,6 +4777,13 @@ "@types/estree": "^1.0.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4895,6 +4981,19 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "9.3.5", "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", @@ -5036,6 +5135,22 @@ "human-id": "dist/cli.js" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5122,6 +5237,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5294,6 +5425,88 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", + "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.0", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^9.0.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -5329,6 +5542,55 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -5427,6 +5689,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -5480,6 +5755,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/nano-spawn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", + "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5634,6 +5922,22 @@ "node": ">=0.10.0" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -5867,6 +6171,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -6170,6 +6487,23 @@ "node": ">=8" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6181,6 +6515,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.44.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", @@ -6442,6 +6783,36 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6514,6 +6885,17 @@ "node": ">=6" } }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -6531,6 +6913,62 @@ "node": ">=10.0.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6620,6 +7058,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -6791,14 +7242,19 @@ "peer": true }, "node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.0.0.tgz", + "integrity": "sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==", + "dev": true, "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { @@ -7200,6 +7656,84 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -7219,6 +7753,19 @@ "optional": true, "peer": true }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index e8c06fd..b87b4e3 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "test:watch": "vitest", "clean": "rm -rf dist", "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run clean && npm run test && npm run build" + "prepublishOnly": "npm run clean && npm run test && npm run build", + "prepare": "husky install" }, "keywords": [ "typescript", @@ -90,13 +91,21 @@ "devDependencies": { "@changesets/cli": "^2.29.6", "@types/node": "^20.19.1", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", "prettier": "^3.6.2", + "type-fest": "^5.0.0", "typescript": "^5.9.2", "vitest": "^3.2.4" }, "engines": { "node": ">=20" }, + "lint-staged": { + "*.{ts,tsx,js,jsx,json}": [ + "prettier --write" + ] + }, "typesVersions": { "*": { "nextjs": [ diff --git a/src/__tests__/Try.test.ts b/src/__tests__/Try.test.ts index cdc5d97..94bc257 100644 --- a/src/__tests__/Try.test.ts +++ b/src/__tests__/Try.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import Try, { TryResult } from '../nextjs'; +import Try from '../nextjs'; +import type { TryResult } from '../core/Try'; // Mock Sentry SDK vi.mock('@sentry/nextjs', () => { @@ -35,7 +36,7 @@ async function successfulFunction( } class TestClass { - private name: string; + private readonly name: string; constructor(name: string) { this.name = name; @@ -428,7 +429,7 @@ describe('Try', () => { }); it('should not log errors by default', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; await new Try(throwingFunction, params).debug(false).value(); @@ -438,7 +439,7 @@ describe('Try', () => { }); it('should log errors when debug is enabled', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; await new Try(throwingFunction, params).debug().value(); @@ -448,7 +449,7 @@ describe('Try', () => { }); it('should not log errors when debug is explicitly disabled', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; await new Try(throwingFunction, params).debug(false).value(); @@ -458,7 +459,7 @@ describe('Try', () => { }); it('should log finally callback errors when debug is enabled', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; const throwingFinally = () => { throw new Error('finally error'); @@ -477,7 +478,7 @@ describe('Try', () => { }); it('should not log finally callback errors when debug is disabled', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; const throwingFinally = () => { throw new Error('finally error'); @@ -490,7 +491,7 @@ describe('Try', () => { }); it('should support conditional debug logging', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; const isDevelopment = true; @@ -559,7 +560,7 @@ describe('Try', () => { }); it('should handle async finally callback errors', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; const throwingAsyncFinally = async () => { @@ -580,7 +581,7 @@ describe('Try', () => { }); it('should handle async finally callback errors without debug', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const params = { parameterKey: 'alpha' }; const throwingAsyncFinally = async () => { diff --git a/src/__tests__/flexible-breadcrumbs.test.ts b/src/__tests__/flexible-breadcrumbs.test.ts index 3a5f1b2..24e8093 100644 --- a/src/__tests__/flexible-breadcrumbs.test.ts +++ b/src/__tests__/flexible-breadcrumbs.test.ts @@ -83,7 +83,7 @@ describe('Flexible Breadcrumbs System', () => { }); it('should use custom transformers', async () => { - function processOrder(orderId: string, amount: number, metadata: any) { + function processOrder(_orderId: string, _amount: number, _metadata: any) { throw new Error('test'); } @@ -91,12 +91,12 @@ describe('Flexible Breadcrumbs System', () => { tags: ['urgent', 'vip'], }) .breadcrumbs( - (id: unknown) => ({ orderId: String(id) }), - (amount: unknown) => ({ - amountCategory: (amount as number) > 100 ? 'large' : 'small', + (id: string) => ({ orderId: id }), + (amount: number) => ({ + amountCategory: amount > 100 ? 'large' : 'small', }), - (meta: unknown) => ({ - tagCount: (meta as { tags: string[] }).tags.length, + (meta: { tags: string[] }) => ({ + tagCount: meta.tags.length, }), ) .value(); @@ -114,7 +114,7 @@ describe('Flexible Breadcrumbs System', () => { }); it('should use predefined transformers', async () => { - function analyzeData(text: string, numbers: number[], config: object) { + function analyzeData(_text: string, _numbers: number[], _config: object) { throw new Error('test'); } @@ -179,7 +179,7 @@ describe('Flexible Breadcrumbs System', () => { }); it('should handle toString transformer', async () => { - function processValues(num: number, bool: boolean, obj: object) { + function processValues(_num: number, _bool: boolean, _obj: object) { throw new Error('test'); } @@ -204,7 +204,7 @@ describe('Flexible Breadcrumbs System', () => { }); it('should handle invalid parameter indices gracefully', async () => { - function twoParams(a: string, b: number) { + function twoParams(_a: string, _b: number) { throw new Error('test'); } @@ -231,9 +231,9 @@ describe('Flexible Breadcrumbs System', () => { describe('Object Syntax Configuration', () => { it('should extract using object syntax with parameter indices', async () => { function processRequest( - endpoint: string, - payload: { userId: number; data: string }, - headers: any, + _endpoint: string, + _payload: { userId: number; data: string }, + _headers: any, ) { throw new Error('test'); } @@ -245,9 +245,11 @@ describe('Flexible Breadcrumbs System', () => { { 'Content-Type': 'application/json' }, ) .breadcrumbs({ - 0: (url: string) => ({ endpoint: url }), + 0: (url: unknown) => ({ endpoint: url as string }), 1: ['userId'], - 2: (headers: Record) => ({ headerCount: Object.keys(headers).length }), + 2: (headers: unknown) => ({ + headerCount: Object.keys(headers as Record).length, + }), }) .value(); @@ -265,9 +267,9 @@ describe('Flexible Breadcrumbs System', () => { it('should handle mixed object and array configurations', async () => { function complexFunction( - id: string, - user: { name: string; age: number }, - settings: any, + _id: string, + _user: { name: string; age: number }, + _settings: any, ) { throw new Error('test'); } @@ -279,9 +281,13 @@ describe('Flexible Breadcrumbs System', () => { { theme: 'dark', notifications: true }, ) .breadcrumbs({ - 0: (id: string) => ({ identifier: id.toUpperCase() }), + 0: (id: unknown) => ({ identifier: (id as string).toUpperCase() }), 1: ['name', 'age'], - 2: (settings: { theme: string; notifications: boolean }) => ({ settingsCount: Object.keys(settings).length }), + 2: (settings: unknown) => ({ + settingsCount: Object.keys( + settings as { theme: string; notifications: boolean }, + ).length, + }), }) .value(); @@ -297,13 +303,51 @@ describe('Flexible Breadcrumbs System', () => { }), ); }); + + it('should skip parameter without transformer', async () => { + function processRequest( + _endpoint: string, + _payload: { userId: number; data: string }, + _token: string, + _headers: any, + ) { + throw new Error('test'); + } + + await new Try( + processRequest, + '/api/users', + { userId: 123, data: 'test' }, + 'sensitive', + { 'Content-Type': 'application/json' }, + ) + .breadcrumbs({ + 0: (url: unknown) => ({ endpoint: url as string }), + 1: ['userId'], + 3: (headers: unknown) => ({ + headerCount: Object.keys(headers as Record).length, + }), + }) + .value(); + + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Calling processRequest function', + data: { + endpoint: '/api/users', + userId: 123, + headerCount: 1, + }, + }), + ); + }); }); describe('Error Handling', () => { it('should handle transformer errors gracefully with debug enabled', async () => { const consoleSpy = vi .spyOn(console, 'error') - .mockImplementation(() => { }); + .mockImplementation(() => {}); function testFunction(data: string) { throw new Error('test'); @@ -338,7 +382,7 @@ describe('Flexible Breadcrumbs System', () => { it('should handle transformer errors gracefully with debug disabled', async () => { const consoleSpy = vi .spyOn(console, 'error') - .mockImplementation(() => { }); + .mockImplementation(() => {}); function testFunction(data: string) { throw new Error('test'); @@ -369,7 +413,7 @@ describe('Flexible Breadcrumbs System', () => { it('should handle predefined transformer errors gracefully', async () => { const consoleSpy = vi .spyOn(console, 'error') - .mockImplementation(() => { }); + .mockImplementation(() => {}); function testFunction(data: any) { throw new Error('test'); diff --git a/src/adapters/browser/reporter.ts b/src/adapters/browser/reporter.ts index ab1ce62..f2bbd60 100644 --- a/src/adapters/browser/reporter.ts +++ b/src/adapters/browser/reporter.ts @@ -7,19 +7,22 @@ import type { Reporter, ErrorReportConfig } from '../../core/reporter'; */ export class BrowserReporter implements Reporter { report(error: Error, config: ErrorReportConfig): void { - const errorToReport = config.message - ? this.createWrappedError(error, config.message) + const errorToReport = config.message + ? this.createWrappedError(error, config.message) : error; - + Sentry.captureException(errorToReport, { - tags: { ...config.tags, library: '@power-rent/try-catch' } + tags: { ...config.tags, library: '@power-rent/try-catch' }, }); } - addBreadcrumbs(data: Record, functionName = 'anonymous'): void { - Sentry.addBreadcrumb({ - message: `Calling ${functionName} function`, - data + addBreadcrumbs( + data: Record, + functionName = 'anonymous', + ): void { + Sentry.addBreadcrumb({ + message: `Calling ${functionName} function`, + data, }); } @@ -29,4 +32,4 @@ export class BrowserReporter implements Reporter { wrapped.stack = error.stack; return wrapped; } -} \ No newline at end of file +} diff --git a/src/adapters/node/reporter.ts b/src/adapters/node/reporter.ts index 1c58c14..a066c31 100644 --- a/src/adapters/node/reporter.ts +++ b/src/adapters/node/reporter.ts @@ -7,19 +7,22 @@ import type { Reporter, ErrorReportConfig } from '../../core/reporter'; */ export class NodeReporter implements Reporter { report(error: Error, config: ErrorReportConfig): void { - const errorToReport = config.message - ? this.createWrappedError(error, config.message) + const errorToReport = config.message + ? this.createWrappedError(error, config.message) : error; - + Sentry.captureException(errorToReport, { - tags: { ...config.tags, library: '@power-rent/try-catch' } + tags: { ...config.tags, library: '@power-rent/try-catch' }, }); } - addBreadcrumbs(data: Record, functionName = 'anonymous'): void { - Sentry.addBreadcrumb({ - message: `Calling ${functionName} function`, - data + addBreadcrumbs( + data: Record, + functionName = 'anonymous', + ): void { + Sentry.addBreadcrumb({ + message: `Calling ${functionName} function`, + data, }); } @@ -29,4 +32,4 @@ export class NodeReporter implements Reporter { wrapped.stack = error.stack; return wrapped; } -} \ No newline at end of file +} diff --git a/src/browser/index.ts b/src/browser/index.ts index 1e7f5df..3961842 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -11,9 +11,9 @@ TryClass.setDefaultReporter(new BrowserReporter()); * * Usage: * import { Try } from '@power-rent/try-catch/browser'; - * + * * const result = new Try(asyncFn, arg1, arg2) * .breadcrumbs(['id']) * .report('failed to execute') * .unwrap(); - */ \ No newline at end of file + */ diff --git a/src/core/Try.ts b/src/core/Try.ts index 6bb2183..f806d50 100644 --- a/src/core/Try.ts +++ b/src/core/Try.ts @@ -5,6 +5,7 @@ import { ValidateKeys, BreadcrumbExtractor as BreadcrumbExtractorType, BreadcrumbExtractorUtil, + BreadcrumbData, } from '../utils'; import { Reporter, NoopReporter, ErrorReportConfig } from './reporter'; @@ -33,13 +34,13 @@ interface TryConfig { */ export type TryResult = | { - readonly success: true; - readonly value: Awaited; - } + readonly success: true; + readonly value: Awaited; + } | { - readonly success: false; - readonly error: Error; - }; + readonly success: false; + readonly error: Error; + }; /** * Core Try class for simplified async error handling. @@ -57,7 +58,7 @@ export class Try { private readonly args: TArgs; private config: TryConfig; private cachedResult?: TryResult; - private cachedBreadcrumbData?: Record; + private cachedBreadcrumbData?: BreadcrumbData; private breadcrumbsAdded: boolean = false; private state: 'pending' | 'executed'; private static ignoreErrorTypes: string[] = []; @@ -112,7 +113,7 @@ export class Try { * .unwrap(); * ``` */ - constructor(fn: ((...args: TArgs) => T | Promise), ...args: TArgs) { + constructor(fn: (...args: TArgs) => T | Promise, ...args: TArgs) { this.fn = fn; this.args = args; this.config = { tags: {} }; @@ -218,25 +219,33 @@ export class Try { * .unwrap(); * ``` */ + breadcrumbs(config: { + [key: number]: BreadcrumbTransformer; + }): Try; + breadcrumbs( - keys: Keys + keys: Keys, ): Try; - breadcrumbs>(...transformers: T): Try; + breadcrumbs>( + ...transformers: T + ): Try; breadcrumbs(config: BreadcrumbOptions): Try; breadcrumbs( configOrFirstTransformer?: | BreadcrumbOptions - | BreadcrumbTransformer, - ...restTransformers: BreadcrumbTransformer[] + | BreadcrumbTransformer + | { [key: number]: BreadcrumbTransformer }, + ...restTransformers: BreadcrumbTransformer[] ): Try { // Handle variadic transformer functions if (typeof configOrFirstTransformer === 'function') { const allTransformers = [configOrFirstTransformer, ...restTransformers]; return this.setConfig({ - breadcrumbConfig: allTransformers as unknown as BreadcrumbOptions, + breadcrumbConfig: + allTransformers as unknown as BreadcrumbOptions, }); } @@ -554,7 +563,7 @@ export class Try { } // Return configured default when error occurs, otherwise undefined - return (this.config.defaultValue as any) ?? undefined; + return (this.config.defaultValue as Awaited | undefined) ?? undefined; } /** @@ -629,7 +638,7 @@ export class Try { /** * Extract breadcrumb data using the flexible configuration. */ - private extractAllBreadcrumbData(): Record { + private extractAllBreadcrumbData(): BreadcrumbData { const config = this.config.breadcrumbConfig!; return BreadcrumbExtractorUtil.extract( config, diff --git a/src/core/index.ts b/src/core/index.ts index e2b852e..4a2b07d 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,4 +3,4 @@ */ export { Try, TryResult } from './Try'; -export { Reporter, NoopReporter, ErrorReportConfig } from './reporter'; \ No newline at end of file +export { Reporter, NoopReporter, ErrorReportConfig } from './reporter'; diff --git a/src/core/reporter.ts b/src/core/reporter.ts index 651d8c3..2cc98c9 100644 --- a/src/core/reporter.ts +++ b/src/core/reporter.ts @@ -1,10 +1,12 @@ +import type { BreadcrumbData } from '../utils/types'; + /** * Configuration for error reporting */ export interface ErrorReportConfig { readonly message?: string; readonly tags: Readonly>; - readonly breadcrumbData?: Record; + readonly breadcrumbData?: BreadcrumbData; readonly functionName?: string; } @@ -25,7 +27,7 @@ export interface Reporter { * @param data The breadcrumb data to add * @param functionName Optional function name for context */ - addBreadcrumbs(data: Record, functionName?: string): void; + addBreadcrumbs(data: BreadcrumbData, functionName?: string): void; /** * Create a wrapped error with a custom message @@ -45,7 +47,7 @@ export class NoopReporter implements Reporter { // Do nothing } - addBreadcrumbs(_data: Record, _functionName?: string): void { + addBreadcrumbs(_data: BreadcrumbData, _functionName?: string): void { // Do nothing } @@ -55,4 +57,4 @@ export class NoopReporter implements Reporter { wrappedError.stack = error.stack; return wrappedError; } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 8e2d836..50bb2a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,13 @@ export type { ErrorReportConfig } from './core/reporter'; export * from './utils/types'; export * from './utils/transformers'; export * from './utils/breadcrumbs'; + +// Explicitly export important breadcrumb types for better discoverability +export type { + BreadcrumbValue, + BreadcrumbData, + BreadcrumbTransformer, + BreadcrumbOptions, + BreadcrumbExtractor, + PredefinedTransformerType, +} from './utils/types'; diff --git a/src/nextjs/SentryReporter.ts b/src/nextjs/SentryReporter.ts index e09f17a..9c8f1f2 100644 --- a/src/nextjs/SentryReporter.ts +++ b/src/nextjs/SentryReporter.ts @@ -11,12 +11,17 @@ export class SentryReporter implements Reporter { */ report(error: Error, config: ErrorReportConfig): void { // Add breadcrumbs if configured - if (config.breadcrumbData && Object.keys(config.breadcrumbData).length > 0) { + if ( + config.breadcrumbData && + Object.keys(config.breadcrumbData).length > 0 + ) { this.addBreadcrumbs(config.breadcrumbData, config.functionName); } // Create wrapped error if custom message is provided - const errorToReport = config.message ? this.createWrappedError(error, config.message) : error; + const errorToReport = config.message + ? this.createWrappedError(error, config.message) + : error; // Report to Sentry with tags Sentry.captureException(errorToReport, { @@ -27,7 +32,10 @@ export class SentryReporter implements Reporter { /** * Add breadcrumbs to Sentry context */ - addBreadcrumbs(data: Record, functionName = 'anonymous'): void { + addBreadcrumbs( + data: Record, + functionName = 'anonymous', + ): void { Sentry.addBreadcrumb({ message: `Calling ${functionName} function`, data, @@ -43,4 +51,4 @@ export class SentryReporter implements Reporter { wrappedError.stack = error.stack; return wrappedError; } -} \ No newline at end of file +} diff --git a/src/nextjs/index.ts b/src/nextjs/index.ts index 945ed01..ff7cb9e 100644 --- a/src/nextjs/index.ts +++ b/src/nextjs/index.ts @@ -1,4 +1,4 @@ -import { Try as CoreTry, TryResult } from '../core/Try'; +import { Try as CoreTry } from '../core/Try'; import { SentryReporter } from './SentryReporter'; // Set up the Sentry reporter as the default for NextJS @@ -14,7 +14,10 @@ CoreTry.setDefaultReporter(new SentryReporter()); * .report('failed to execute') * .unwrap(); */ -export class Try extends CoreTry { +export class Try< + T, + TArgs extends readonly unknown[] = unknown[], +> extends CoreTry { /** * Configure error types that should be thrown through without being wrapped. * When using `.report()`, errors matching these types will be re-thrown as-is diff --git a/src/node/index.ts b/src/node/index.ts index 4335c87..b838c56 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -11,9 +11,9 @@ TryClass.setDefaultReporter(new NodeReporter()); * * Usage: * import { Try } from '@power-rent/try-catch/node'; - * + * * const result = new Try(asyncFn, arg1, arg2) * .breadcrumbs(['id']) * .report('failed to execute') * .unwrap(); - */ \ No newline at end of file + */ diff --git a/src/utils/breadcrumbs.ts b/src/utils/breadcrumbs.ts index 6f6ab17..c5ea9ee 100644 --- a/src/utils/breadcrumbs.ts +++ b/src/utils/breadcrumbs.ts @@ -3,6 +3,8 @@ import type { BreadcrumbExtractor, BreadcrumbConfig, BreadcrumbTransformer, + BreadcrumbData, + BreadcrumbValue, } from './types'; import { TransformerRegistry } from './transformers'; @@ -18,16 +20,16 @@ export class BreadcrumbExtractorUtil { config: BreadcrumbOptions, args: TArgs, debug = false, - ): Record { - let breadcrumbData: Record = {}; + ): BreadcrumbData { + let breadcrumbData: BreadcrumbData = {}; // Array of keys from first parameter if (this.isStringArray(config)) { const firstArg = args[0]; if (firstArg && typeof firstArg === 'object') { breadcrumbData = this.extractFromKeys( - firstArg, - config as readonly (keyof TArgs[0])[], + firstArg as Record, + config as readonly (string | number)[], ); } } @@ -66,15 +68,15 @@ export class BreadcrumbExtractorUtil { * Extract breadcrumb data from object using specified keys */ static extractFromKeys( - obj: any, - keys: readonly (keyof any)[], - ): Record { - const breadcrumbData: Record = {}; + obj: Record, + keys: readonly (string | number)[], + ): BreadcrumbData { + const breadcrumbData: BreadcrumbData = {}; keys.forEach((key) => { const value = obj[key]; - if (value !== undefined) { - breadcrumbData[key as string] = value; + if (value !== undefined && this.isValidBreadcrumbValue(value)) { + breadcrumbData[String(key)] = value; } }); @@ -88,7 +90,7 @@ export class BreadcrumbExtractorUtil { extractor: BreadcrumbExtractor, args: TArgs, debug = false, - ): Record { + ): BreadcrumbData { if ( !TransformerRegistry.validateParameterIndex(extractor.param, args.length) ) { @@ -100,7 +102,10 @@ export class BreadcrumbExtractorUtil { if ('keys' in extractor) { // Extract specific keys from object if (paramValue && typeof paramValue === 'object') { - return this.extractFromKeys(paramValue, extractor.keys); + return this.extractFromKeys( + paramValue as Record, + extractor.keys, + ); } } else if ('transform' in extractor) { // Apply custom transformer @@ -140,19 +145,42 @@ export class BreadcrumbExtractorUtil { ); } + /** + * Validates if a value can be safely used as breadcrumb data + */ + private static isValidBreadcrumbValue( + value: unknown, + ): value is BreadcrumbValue { + if (value === null || value === undefined) return true; + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) + return true; + if (Array.isArray(value)) + return value.every((item) => this.isValidBreadcrumbValue(item)); + if (typeof value === 'object') { + return Object.values(value as Record).every((v) => + this.isValidBreadcrumbValue(v), + ); + } + return false; + } + /** * Extract breadcrumb data from array configuration */ private static extractFromArray( config: readonly ( | string - | readonly string[] + | readonly (string | number)[] | BreadcrumbExtractor )[], args: TArgs, debug: boolean, - ): Record { - let breadcrumbData: Record = {}; + ): BreadcrumbData { + let breadcrumbData: BreadcrumbData = {}; config.forEach((entry, index) => { // If entry is a plain extractor object with its own param index, use existing logic @@ -170,11 +198,16 @@ export class BreadcrumbExtractorUtil { const arg = args[index]; if (typeof entry === 'string') { // Map value directly under the provided key - breadcrumbData = { ...breadcrumbData, [entry]: arg }; + if (this.isValidBreadcrumbValue(arg)) { + breadcrumbData = { ...breadcrumbData, [entry]: arg }; + } } else if (Array.isArray(entry)) { // Extract listed keys from an object argument if (arg && typeof arg === 'object') { - const data = this.extractFromKeys(arg, entry); + const data = this.extractFromKeys( + arg as Record, + entry, + ); breadcrumbData = { ...breadcrumbData, ...data }; } } @@ -190,8 +223,8 @@ export class BreadcrumbExtractorUtil { config: BreadcrumbConfig, args: TArgs, debug: boolean, - ): Record { - let breadcrumbData: Record = {}; + ): BreadcrumbData { + let breadcrumbData: BreadcrumbData = {}; for (const [paramIndex, paramConfig] of Object.entries(config)) { const index = parseInt(paramIndex, 10); @@ -214,16 +247,19 @@ export class BreadcrumbExtractorUtil { */ private static extractFromParameterConfig( paramIndex: number, - config: readonly (keyof any)[] | BreadcrumbTransformer, + config: readonly (string | number)[] | BreadcrumbTransformer, args: TArgs, debug: boolean, - ): Record { + ): BreadcrumbData { const paramValue = args[paramIndex]; if (Array.isArray(config)) { // Extract keys from object if (paramValue && typeof paramValue === 'object') { - return this.extractFromKeys(paramValue, config); + return this.extractFromKeys( + paramValue as Record, + config, + ); } } else if (typeof config === 'function') { // Apply transformer function diff --git a/src/utils/error-reporter.ts b/src/utils/error-reporter.ts index 9046480..a9feec3 100644 --- a/src/utils/error-reporter.ts +++ b/src/utils/error-reporter.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/nextjs'; +import type { BreadcrumbData } from './types'; /** * Configuration for error reporting @@ -6,14 +7,14 @@ import * as Sentry from '@sentry/nextjs'; export interface ErrorReportConfig { readonly message?: string; readonly tags: Readonly>; - readonly breadcrumbData?: Record; + readonly breadcrumbData?: BreadcrumbData; readonly functionName?: string; } /** * @deprecated This class is deprecated and will be removed in a future version. * Use the Reporter interface from '../core/reporter' instead for better modularity. - * + * * Utility class for handling error reporting to Sentry */ export class ErrorReporter { @@ -22,12 +23,17 @@ export class ErrorReporter { */ static report(error: Error, config: ErrorReportConfig): void { // Add breadcrumbs if configured - if (config.breadcrumbData && Object.keys(config.breadcrumbData).length > 0) { + if ( + config.breadcrumbData && + Object.keys(config.breadcrumbData).length > 0 + ) { this.addBreadcrumbs(config.breadcrumbData, config.functionName); } // Create wrapped error if custom message is provided - const errorToReport = config.message ? this.createWrappedError(error, config.message) : error; + const errorToReport = config.message + ? this.createWrappedError(error, config.message) + : error; // Report to Sentry with tags Sentry.captureException(errorToReport, { @@ -38,7 +44,10 @@ export class ErrorReporter { /** * Add breadcrumbs to Sentry context */ - static addBreadcrumbs(data: Record, functionName = 'anonymous'): void { + static addBreadcrumbs( + data: BreadcrumbData, + functionName = 'anonymous', + ): void { Sentry.addBreadcrumb({ message: `Calling ${functionName} function`, data, @@ -58,7 +67,10 @@ export class ErrorReporter { /** * Check if an error type should be thrown through without wrapping */ - static shouldThrowThrough(error: Error, ignoreErrorTypes: readonly string[]): boolean { + static shouldThrowThrough( + error: Error, + ignoreErrorTypes: readonly string[], + ): boolean { return ignoreErrorTypes.includes(error.name); } -} \ No newline at end of file +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 9b9aa53..ccf4d87 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,4 +5,18 @@ export * from './types'; export * from './transformers'; export * from './breadcrumbs'; -export * from './error-reporter'; \ No newline at end of file +export * from './error-reporter'; + +// Explicitly export important types for easier consumption +export type { + BreadcrumbValue, + BreadcrumbData, + BreadcrumbTransformer, + BreadcrumbOptions, + BreadcrumbExtractor, + PredefinedTransformerType, + ValidateKeys, + BreadcrumbConfig, + PositionalBreadcrumbs, + VariadicBreadcrumbTransformers, +} from './types'; diff --git a/src/utils/transformers.ts b/src/utils/transformers.ts index b3e5292..1bdd874 100644 --- a/src/utils/transformers.ts +++ b/src/utils/transformers.ts @@ -1,4 +1,9 @@ -import type { BreadcrumbTransformer } from './types'; +import type { + BreadcrumbTransformer, + BreadcrumbData, + BreadcrumbValue, + PredefinedTransformerType, +} from './types'; import { BreadcrumbTransformationError } from './types'; /** @@ -8,38 +13,42 @@ export const PredefinedTransformers = { /** * Extract length of strings, arrays, or object keys */ - length: (value: unknown, paramIndex: number): Record => { + length: (value: unknown, paramIndex: number): BreadcrumbData => { const paramKey = `param${paramIndex}`; - + if (typeof value === 'string' || Array.isArray(value)) { return { [`${paramKey}_length`]: value.length }; } else if (value && typeof value === 'object') { return { [`${paramKey}_length`]: Object.keys(value).length }; } - + return {}; }, /** * Get the type of the value */ - type: (value: unknown, paramIndex: number): Record => { + type: (value: unknown, paramIndex: number): BreadcrumbData => { const paramKey = `param${paramIndex}`; return { [`${paramKey}_type`]: typeof value }; }, /** - * Include the raw value + * Include the raw value (with validation) */ - value: (value: unknown, paramIndex: number): Record => { + value: (value: unknown, paramIndex: number): BreadcrumbData => { const paramKey = `param${paramIndex}`; - return { [`${paramKey}_value`]: value }; + // Only include JSON-serializable values + if (TransformerRegistry.isSerializable(value)) { + return { [`${paramKey}_value`]: value as BreadcrumbValue }; + } + return {}; }, /** * Convert value to string representation */ - toString: (value: unknown, paramIndex: number): Record => { + toString: (value: unknown, paramIndex: number): BreadcrumbData => { const paramKey = `param${paramIndex}`; return { [`${paramKey}_string`]: String(value) }; }, @@ -49,14 +58,38 @@ export const PredefinedTransformers = { * Registry for managing breadcrumb transformers */ export class TransformerRegistry { + /** + * Check if a value is JSON-serializable + */ + static isSerializable(value: unknown): boolean { + if (value === null || value === undefined) return true; + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) + return true; + if (Array.isArray(value)) + return value.every((item) => this.isSerializable(item)); + if (typeof value === 'object') { + try { + JSON.stringify(value); + return true; + } catch { + return false; + } + } + return false; + } + /** * Apply a custom transformer function safely */ static apply( - transformer: BreadcrumbTransformer, + transformer: BreadcrumbTransformer, value: unknown, debug = false, - ): Record { + ): BreadcrumbData { try { return transformer(value); } catch (error) { @@ -64,11 +97,11 @@ export class TransformerRegistry { error as Error, 'custom', ); - + if (debug) { console.error('Error in breadcrumb transformer:', transformationError); } - + return {}; } } @@ -77,11 +110,11 @@ export class TransformerRegistry { * Apply a predefined transformer by type */ static applyPredefined( - transformerType: 'length' | 'type' | 'value' | 'toString', + transformerType: PredefinedTransformerType, value: unknown, paramIndex: number, debug = false, - ): Record { + ): BreadcrumbData { try { const transformer = PredefinedTransformers[transformerType]; return transformer(value, paramIndex); @@ -91,11 +124,11 @@ export class TransformerRegistry { transformerType, paramIndex, ); - + if (debug) { console.error('Error in predefined transformer:', transformationError); } - + return {}; } } @@ -103,7 +136,10 @@ export class TransformerRegistry { /** * Validate that a parameter index is valid for the given arguments */ - static validateParameterIndex(paramIndex: number, argsLength: number): boolean { + static validateParameterIndex( + paramIndex: number, + argsLength: number, + ): boolean { return paramIndex >= 0 && paramIndex < argsLength; } -} \ No newline at end of file +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 6ce3a23..0900e08 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -2,10 +2,29 @@ * Shared type definitions for breadcrumb system */ +import type { Simplify, IsUnknown, Primitive } from 'type-fest'; + +/** + * Base type for breadcrumb data - only JSON-serializable values are allowed + */ +export type BreadcrumbValue = + | string + | number + | boolean + | null + | undefined + | ReadonlyArray + | { readonly [key: string]: BreadcrumbValue }; + +/** + * Breadcrumb data object with string keys and JSON-serializable values + */ +export type BreadcrumbData = Simplify>; + /** - * Breadcrumb transformation function type + * Breadcrumb transformation function type that must return valid breadcrumb data */ -export type BreadcrumbTransformer = (value: T) => Record; +export type BreadcrumbTransformer = (value: T) => BreadcrumbData; /** * Utility type that validates keys exist on the first parameter type. @@ -13,12 +32,13 @@ export type BreadcrumbTransformer = (value: T) => Record; */ export type ValidateKeys< TArgs extends readonly unknown[], - Keys extends readonly string[] -> = TArgs[0] extends Record - ? Keys extends readonly (keyof TArgs[0])[] - ? Keys - : never - : never; + Keys extends readonly (string | number)[], +> = + TArgs[0] extends Record + ? Keys extends readonly (keyof TArgs[0])[] + ? Keys + : never + : never; /** * Utility type that creates a variadic tuple of breadcrumb transformers. @@ -31,46 +51,62 @@ export type ValidateKeys< */ export type VariadicBreadcrumbTransformers = TArgs extends readonly [infer First, ...infer Rest] - ? readonly [BreadcrumbTransformer?, ...VariadicBreadcrumbTransformers] - : readonly []; + ? readonly [ + BreadcrumbTransformer?, + ...VariadicBreadcrumbTransformers, + ] + : readonly []; /** - * Breadcrumb extractor configuration for array syntax + * Predefined transformer types for common use cases */ -export type BreadcrumbExtractor = +export type PredefinedTransformerType = + | 'length' + | 'type' + | 'value' + | 'toString'; + +/** + * Breadcrumb extractor configuration for array syntax with improved type safety + */ +export type BreadcrumbExtractor< + TArgs extends readonly unknown[] = readonly unknown[], +> = | { - // Extract from object parameters by key - readonly param: number; - readonly keys: readonly string[]; - } + // Extract from object parameters by key with type validation + readonly param: number; + readonly keys: readonly (string | number)[]; + } | { - // Transform any parameter to breadcrumbs - readonly param: number; - readonly transform: BreadcrumbTransformer; - } + // Transform any parameter to breadcrumbs with typed transformer + readonly param: number; + readonly transform: BreadcrumbTransformer; + } | { - // Predefined transformer for common types - readonly param: number; - readonly as: 'length' | 'type' | 'value' | 'toString'; - }; + // Predefined transformer for common types + readonly param: number; + readonly as: PredefinedTransformerType; + }; -// Positional breadcrumb syntax where each entry corresponds to the argument position -// - string => maps the argument value to the given breadcrumb key -// - BreadcrumbExtractor => advanced per-entry extractor (still supported) -// Note: string[] is intentionally excluded here to force key validation through ValidateKeys +/** + * Positional breadcrumb syntax where each entry corresponds to the argument position + * - string => maps the argument value to the given breadcrumb key + * - BreadcrumbExtractor => advanced per-entry extractor (still supported) + * Note: string[] is intentionally excluded here to force key validation through ValidateKeys + */ export type PositionalBreadcrumbs = readonly ( | string | BreadcrumbExtractor )[]; /** - * Object-style breadcrumb configuration + * Object-style breadcrumb configuration with improved type safety */ -export type BreadcrumbConfig = { - readonly [K in number | string]?: - | readonly string[] - | BreadcrumbTransformer; -}; +export type BreadcrumbConfig = Simplify<{ + readonly [K in number]?: + | readonly (string | number)[] + | BreadcrumbTransformer; +}>; /** * Union type for all breadcrumb configuration options @@ -90,7 +126,9 @@ export class BreadcrumbTransformationError extends Error { public readonly transformerType: string, public readonly paramIndex?: number, ) { - super(`Breadcrumb transformation failed (${transformerType}): ${originalError.message}`); + super( + `Breadcrumb transformation failed (${transformerType}): ${originalError.message}`, + ); this.name = 'BreadcrumbTransformationError'; } } diff --git a/tsconfig.json b/tsconfig.json index 8868f11..e2a9bb7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "moduleResolution": "node" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"] + "exclude": ["node_modules", "dist"] }