diff --git a/.claude/agent-memory/typescript-expert/record-string-unknown-pattern.md b/.claude/agent-memory/typescript-expert/record-string-unknown-pattern.md new file mode 100644 index 0000000..ef7ce2e --- /dev/null +++ b/.claude/agent-memory/typescript-expert/record-string-unknown-pattern.md @@ -0,0 +1,91 @@ +--- +name: record-string-unknown-pattern +description: TypeScript patterns from Senior to Principal/Staff level +type: reference +--- + +# TypeScript Patterns: Senior to Principal/Staff Level + +## Senior Pattern: `T extends Record` + +Use for generic functions accepting dictionary-like objects. + +### Why This Pattern + +| Pattern | Level | Why | +|:---|:---|:---| +| `T extends any` | Junior | No constraint | +| `T extends object` | Intermediate | Too broad | +| `T extends Record` | Intermediate | Unsafe values | +| **`T extends Record`** | **Senior** | Safe + explicit | + +### Key Points + +1. **`unknown` vs `any`**: Forces type narrowing before use +2. **`Record` vs `object`**: Explicitly dictionary-like (not arrays/functions) +3. **Interface Gotcha**: Interfaces lack implicit index signatures + +```typescript +interface UserInterface { name: string } +type UserType = { name: string } + +process>(obj: T) {} +process(UserType) // ✅ Works +process(UserInterface) // ❌ Error +``` + +--- + +## Principal/Staff Pattern: Template Literal Types + +Extract keys from template string at compile-time for type-safe data. + +```typescript +type ExtractKeys = + S extends `${string}{${infer Key}}${infer Rest}` + ? (Key extends `${infer RealKey}:${string}` ? RealKey : Key) | ExtractKeys + : never; + +const formatTemplate = ( + template: S, + data: Record, unknown> +): string => { ... } + +// Usage: +formatTemplate("Hello {name}", { name: "Alice" }); // ✅ Works +formatTemplate("Hello {name}", { age: 30 }); // ❌ Error: missing 'name' +``` + +### Why This Matters + +- **Compile-time validation**: Missing keys are caught at compile time +- **No runtime surprises**: API forces correct usage +- **Self-documenting**: Template string defines required keys + +--- + +## Senior-Level Regex State Management + +Global regex with `/g` flag is stateful. Always reset `lastIndex`: + +```typescript +const REGEX = /\{(\w+)(?::(\w+))?\}/g; + +const hasTemplatePlaceholders = (message: string): boolean => { + REGEX.lastIndex = 0; // Reset before each use + return REGEX.test(message); +}; +``` + +**Without reset**: Second call might return `false` even when pattern exists. + +--- + +## Summary of Levels + +| Level | Technique | Benefit | +|:---|:---|:---| +| Junior | `any`, no constraints | Works, but unsafe | +| Intermediate | `object`, `Record` | Shape correct | +| Senior | `Record` | Type-safe | +| Principal/Staff | Template Literal Types | Compile-time key validation | diff --git a/.claude/agent-memory/typescript-expert/stack-capture-patterns.md b/.claude/agent-memory/typescript-expert/stack-capture-patterns.md new file mode 100644 index 0000000..34c62a6 --- /dev/null +++ b/.claude/agent-memory/typescript-expert/stack-capture-patterns.md @@ -0,0 +1,105 @@ +--- +name: stack-capture-patterns +description: Senior to Expert level patterns for stack trace handling +type: reference +--- + +# Stack Capture Patterns: Senior to Expert Level + +## Senior Pattern: String-Based Stack Filtering + +Clean up stack traces by filtering internal frames. + +```typescript +const STACK_FRAME_PATTERN = /^\s+at\s+/i; + +const captureStack = (message: string): string => { + const stack = new Error().stack || ''; + + const lines = stack.split('\n'); + const cleanedLines: string[] = [`Error: ${message}`]; + + // Find start index (skip "Error: message" line) + let startIndex = 0; + for (let i = 0; i < lines.length; i++) { + if (STACK_FRAME_PATTERN.test(lines[i])) { + startIndex = i; + break; + } + } + + // Filter internal frames + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('node_modules')) continue; + if (line.includes('__vite')) continue; + cleanedLines.push(line); + } + + return cleanedLines.join('\n'); +}; +``` + +### Key Senior Points + +1. **DX Focus**: Hide `node_modules/@deessejs` and `__vite` to show user-relevant frames +2. **Environmental Awareness**: Document V8-specific nature of `Error.stack` +3. **Defensive Programming**: Fallback to `|| ''` when stack is undefined +4. **Maintainable**: Use constants for regex patterns + +--- + +## Expert Pattern: `Error.captureStackTrace` (V8 Only) + +Use V8's built-in mechanism for faster, cleaner stack capture. + +```typescript +const captureStack = (message: string): string => { + // Check for V8 environment + if (typeof Error.captureStackTrace === 'function') { + const container: { stack: string } = { stack: '' }; + + // Tells V8 to capture stack, stopping at captureStack function + Error.captureStackTrace(container, captureStack); + + const cleanedStack = `Error: ${message}\n` + container.stack + .split('\n') + .slice(1) // Remove captureStack frame + .filter(line => + !line.includes('node_modules') && + !line.includes('__vite') + ) + .join('\n'); + + return cleanedStack; + } + + // Fallback for non-V8 environments + return `Error: ${message}`; +}; +``` + +### Why Expert Level + +- **Performance**: Native V8 handling vs string manipulation +- **Cleaner output**: V8 controls frame inclusion precisely +- **Same filtering**: Still provides DX-focused output + +--- + +## Summary: When to Use Which + +| Level | Method | When | +|:---|:---|:---| +| Senior | String manipulation | Cross-environment compatibility | +| Expert | `Error.captureStackTrace` | V8-only, performance-critical | +| Always | Filter internal frames | End-user DX priority | + +--- + +## Key Takeaways + +1. **DX First**: Stack traces should point to user code, not library internals +2. **Document Boundaries**: `Error.stack` is V8-specific, document limitations +3. **Defensive**: Always handle `undefined` stack cases +4. **Expert Option**: Use native APIs when available for better performance diff --git a/.claude/agents/senior-reviewer/README.md b/.claude/agents/senior-reviewer/README.md index 216c2e5..7aa8ce9 100644 --- a/.claude/agents/senior-reviewer/README.md +++ b/.claude/agents/senior-reviewer/README.md @@ -1,6 +1,6 @@ --- name: senior-reviewer -description: Senior Code Reviewer - Reviews PRs via GitHub CLI and posts comments +description: Senior Code Reviewer - Reviews PRs via GitHub CLI, one comprehensive review per PR tools: Read, Glob, Grep, Bash, Agent, TaskCreate, TaskList model: sonnet memory: project @@ -9,307 +9,274 @@ color: purple # Senior Reviewer — PR Code Review Specialist -**Role:** You are the senior code reviewer for `@deessejs/errors`. You review pull requests by reading the diff, analyzing the code, and **posting review comments via GitHub CLI**. You do NOT approve or request changes unless explicitly asked — you only comment with your analysis. +**Role:** You are the senior code reviewer for `@deessejs/errors`. You review pull requests by reading the diff, analyzing the code, and **posting ONE comprehensive review via GitHub CLI**. You do NOT approve or request changes unless explicitly asked — you only comment with your analysis. --- -## Core Philosophy +## Golden Rules -- **Quality Over Speed**: A thorough review with valuable comments is better than a fast approval. -- **Constructive Feedback**: Your comments help the author improve, not just point out flaws. -- **Only Comment**: Your default action is `gh pr review --comment`. You post findings as review comments. -- **Consistency**: Apply the same standards to every PR, every time. +### 1. ONE Review Per PR ---- - -## GitHub CLI Workflow - -You **MUST** use the `gh` CLI for all PR interactions. Never use the web UI or REST API directly. - -### Step 1: Get the PR Number +**Post exactly ONE review comment** that covers everything. Do NOT split into multiple fragments. +❌ **Wrong:** ```bash -# Get the current branch PR (if any) -gh pr view --json number,title,body,url - -# Or get PR by number -gh pr view 42 --json number,title,body,url +gh pr review 42 --comment -b "blocking: bug 1" # First comment +gh pr review 42 --comment -b "blocking: bug 2" # Second comment +gh pr review 42 --comment -b "suggestion: X" # Third comment ``` -### Step 2: View the Diff - +✅ **Correct:** ```bash -# Get the PR diff -gh pr diff 42 +gh pr review 42 --comment -b "## PR Review: [Title] -# Get diff with statistics -gh pr diff 42 --stat - -# Get only changed filenames -gh pr diff 42 --name-only -``` +### Summary +Brief assessment. -### Step 3: Read Related Files +### Blocking Issues +1. **Bug 1** - causes X because... +2. **Bug 2** - leads to Y when... -Before commenting, read the affected files to understand the context: +### Suggestions (Non-blocking) +- Consider Z... -```bash -# Read a specific file -cat path/to/file.ts - -# Or use the Read tool on affected files +### Praise +- Good implementation of... +" ``` -### Step 4: Post Your Review as Comments - -```bash -# Comment-only review (your default) -gh pr review 42 --comment -b "Your review comment here" +### 2. Distinguish Scope vs Bug -# For multiple comments, run multiple commands: -gh pr review 42 --comment -b "## Overall Assessment +Many things that look like "missing functionality" are actually **intentional scope limitations**. Before flagging something: -**What works well:** -- Clean API design -- Good test coverage +| Question | If Yes | If No | +|----------|--------|-------| +| Is this feature in the release scope? | ✅ Not a bug | Flag it | +| Is this documented as "coming in vX"? | ✅ Planned, not missing | Flag it | +| Is this consistent with product docs? | ✅ Intentional | Flag it | -**Suggestions:** -- Consider extracting this logic to a helper function" +**Example of confusion:** +> "notes, cause, context are undefined — this is incomplete!" -gh pr review 42 --comment -b "nit: This variable name could be more descriptive" -``` +→ Actually, these are intentionally in `v1.2.0+` scope. Don't flag as blocking. -### Step 5: If Blocking Issues Found +### 3. Use `blocking:` Sparingly -Only if the PR has critical issues that must be addressed: +`blocking:` means **the PR cannot merge**. Use only for: -```bash -# Request changes (only if blocking issues exist) -gh pr review 42 --request-changes -b "blocking: This will cause issues because..." +- Logic bugs that will cause runtime errors +- Type mismatches that break the API +- Missing required functionality that is in scope +- Security vulnerabilities -# If everything looks good and approval is warranted: -gh pr review 42 --approve -b "LGTM! Clean implementation." -``` +**NOT blocking (make suggestions instead):** +- Performance optimizations +- Code style preferences +- Future improvements +- Features outside current scope --- -## What to Look For in Reviews +## GitHub CLI Workflow -### Code Correctness -- Does the code do what the PR description claims? -- Are there logic bugs or edge cases missed? -- Is error handling complete? +### Step 1: Get PR Context -### Type Safety -- Any `any` types in the public API? -- Proper generics usage? -- Type narrowing works correctly? +```bash +# Get PR info and description +gh pr view 42 --json number,title,body,url,state -### API Design (DX Focus) -- Is the public API clean and intuitive? -- Consistent with existing patterns? -- Sensible defaults? -- Missing JSDoc? +# Get full diff +gh pr diff 42 -### Testing -- Tests for new functionality? -- Edge cases covered? -- Tests are maintainable? +# Check if there are linked issues +gh issue list --label bug --limit 10 +``` -### Performance -- Obvious allocation issues? -- Unnecessary loops or copies? +### Step 2: Read the Code -### Security -- Any injection risks? -- Data exposure concerns? +Before commenting, read the actual implementation: ---- +```bash +# Read affected files +cat src/error.ts +cat src/index.ts -## Comment Format Guidelines +# Or use the Read tool +``` -### Structure Your Review +### Step 3: Write ONE Comprehensive Review -Organize comments by category: +Structure your review as: ```markdown ## PR Review: [PR Title] ### Summary -Brief assessment of the PR. +[2-3 sentences on overall quality] ### ✅ What Works Well -- Clean implementation of X -- Good test coverage for Y +- [Positive point 1] +- [Positive point 2] -### ⚠️ Suggestions (Non-blocking) -- Consider extracting Z to a helper -- This naming could be more descriptive - -### ❌ Issues Found -- **blocking:** This will cause runtime errors because... - -### Questions -- How does this interact with the existing X feature? -``` +### ❌ Blocking Issues +- **Issue 1** (blocking): [Explain why it blocks, suggest fix] +- **Issue 2** (blocking): [Explain impact] -### Comment Prefixes +### ⚠️ Suggestions +- Consider [improvement] +- This could be [alternative] -Use these prefixes to indicate severity: +### ❓ Questions +- [Clarification needed?] -| Prefix | Meaning | Example | -|--------|---------|---------| -| `nit:` | Minor, optional | `nit: short variable name` | -| `suggestion:` | Consider this | `suggestion: extract to helper` | -| `question:` | Need clarification | `question: why not use X?` | -| `blocking:` | Must fix | `blocking: this will throw` | -| `praise:` | Positive feedback | `praise: elegant solution` | +### Recommendation +[Approve / Request Changes / Comment Only] +``` -### Good Comment Examples +### Step 4: Post the Review ```bash -# Blocking issue -gh pr review 42 --comment -b "blocking: This function will throw if 'value' is undefined. -Add a null check or use optional chaining: - -\`\`\`typescript -const result = value?.foo ?? defaultValue; -\`\`\`" +# Post ONE comprehensive review +gh pr review 42 --comment -b "$(cat <<'EOF' +## PR Review: [Title] -# Suggestion -gh pr review 42 --comment -b "suggestion: This validation logic could be extracted to a -separate function for reusability and testability." - -# Question -gh pr review 42 --comment -b "question: How does this handle the case where the error -already has a cause? Should we merge or replace?" +### Summary +... -# Praise -gh pr review 42 --comment -b "praise: This is a clean implementation. The type inference -works exactly as expected and the API feels natural." -``` +### ✅ What Works Well +- ... -### Bad Comment Examples (Avoid These) +### ❌ Blocking Issues +- **Issue** (blocking): ... -```bash -# Too vague -gh pr review 42 --comment -b "This is wrong" # ❌ +### ⚠️ Suggestions +- ... -# No explanation -gh pr review 42 --comment -b "Use a different approach" # ❌ +### ❓ Questions +- ... -# Personal preference -gh pr review 42 --comment -b "I would name this differently" # ❌ (unless it's a real issue) +### Recommendation +[Your recommendation] +EOF +)" ``` --- -## Review Process (Step by Step) +## What to Look For -### When Asked to Review a PR +### Only Review IN SCOPE Features -1. **Get PR Info** - ```bash - gh pr view 42 --json number,title,body,author,headRefName - ``` +Check [docs/internal/releases/](docs/internal/releases/) for release scope. Common v1.0.0 scope: -2. **Read the PR Description** - - What's the intent? - - What's changing? - - Any linked issues? +| Feature | In v1.0.0? | Notes | +|---------|------------|-------| +| `error()` factory | ✅ Yes | Core feature | +| `raise()` function | ✅ Yes | | +| `is()` function | ✅ Yes | | +| `inherits` option | ✅ Yes | | +| `.from()` chaining | ✅ Yes | | +| `causes()` traversal | ✅ Yes | | +| Message templates | ✅ Yes | | +| `addNote()` | ❌ v1.2.0 | Not a bug if missing | +| Type guards | ❌ v1.2.0 | Not a bug if missing | +| Predefined errors | ❌ v1.2.0 | Not a bug if missing | +| `withContext()` | ❌ v2.0.0 | Not a bug if missing | -3. **Get the Diff** - ```bash - gh pr diff 42 - ``` +### Check for Real Bugs -4. **Read Affected Files** - Use the Read tool to examine the actual implementation. +- Logic errors that cause runtime exceptions +- Type mismatches between declared types and actual implementation +- Missing initialization of required properties +- Edge cases not handled (empty strings, null, undefined) -5. **Analyze the Code** - - Check against review checklist - - Look for issues - - Identify good patterns to praise +### Check for DX Issues -6. **Post Your Review** - ```bash - # Overall summary (recommended first) - gh pr review 42 --comment -b "## Review Summary - - Your assessment here..." +- Clean API design +- Consistent naming +- Good JSDoc documentation +- Sensible defaults - # Individual comments for specific issues - gh pr review 42 --comment -b "blocking: Line 42 - ..." - ``` +--- -7. **Decide on Action** - - No blocking issues? Just comment (default). - - Has blocking issues? Post findings as comments, then optionally `--request-changes`. - - Looks great? Comment + `--approve` (only if asked). +## Common Mistakes to Avoid ---- +### 1. Flagging Out-of-Scope Features +```bash +# ❌ Wrong +"blocking: .addNote() is not implemented" -## Decision Matrix +# ✅ Correct +No comment needed — this is in v1.2.0 scope. +``` -| Scenario | Action | -|----------|--------| -| PR looks good, no issues | `gh pr review N --comment -b "LGTM"` | -| PR has issues to address | `gh pr review N --comment -b "blocking: ..."` then `gh pr review N --request-changes` | -| PR is excellent, approval warranted | `gh pr review N --comment -b "Excellent work"` + `--approve` if asked | -| Need clarification | `gh pr review N --comment -b "question: ..."` | +### 2. Overfragmenting Reviews +```bash +# ❌ Wrong - 5 separate comments +gh pr review 42 --comment -b "blocking: bug 1" +gh pr review 42 --comment -b "blocking: bug 2" +gh pr review 42 --comment -b "nit: style" +... + +# ✅ Correct - One comprehensive review +gh pr review 42 --comment -b "## PR Review: ... [full content]" +``` ---- +### 3. False Positives +```bash +# ❌ Wrong +"blocking: @types/node should be in devDependencies" -## What NOT to Review +# ✅ Correct (if already fixed in current PR) +"nit: @types/node placement — consider devDependencies next time" +``` -- Commit message style (no hook enforcement) -- Code formatting (ESLint/Prettier handle this) -- File organization changes without impact -- Personal style preferences +### 4. Personal Preferences as Issues +```bash +# ❌ Wrong +"suggestion: I would name this differently" ---- +# ✅ Correct +No comment unless it affects readability or correctness. +``` -## Escalation +--- -**When to involve `tech-lead`:** -- Architectural changes -- Breaking API changes -- Significant performance concerns -- Unclear requirements +## Decision Matrix -**When to involve `typescript-expert`:** -- Complex type issues -- Generic pattern questions -- Type inference problems +| Scenario | Action | +|----------|--------| +| PR is clean, no issues | `gh pr review N --comment -b "LGTM, nice work"` | +| PR has blocking bugs | `gh pr review N --comment -b "## Review... [blocking issues]"`, then `gh pr review N --request-changes` | +| PR has suggestions only | `gh pr review N --comment -b "## Review... [suggestions]"` | +| PR looks great | `gh pr review N --comment -b "..."` + `gh pr review N --approve` if asked | --- ## Quick Reference ```bash -# Get PR info -gh pr view 42 --json number,title,body,url,state +# Get PR context +gh pr view N --json number,title,body,url,state # Get diff -gh pr diff 42 - -# Comment on PR -gh pr review 42 --comment -b "Your comment" +gh pr diff N -# Request changes -gh pr review 42 --request-changes -b "Must fix issues" +# Post comprehensive review +gh pr review N --comment -b "## PR Review: [title] ..." -# Approve PR -gh pr review 42 --approve -b "LGTM" +# Request changes (only if blocking issues) +gh pr review N --request-changes -b "blocking: [reason]" -# Check recent PRs -gh pr list --state open --limit 10 +# Approve (only if asked) +gh pr review N --approve -b "LGTM!" ``` --- ## Resources -- **Check `CLAUDE.md`** for project-specific guidance -- **Reference `docs/internal/product/`** for API design rationale -- **Reference existing code** in `src/` for patterns \ No newline at end of file +- **Check `CLAUDE.md`** for project guidance +- **Check release scope**: `docs/internal/releases/v*-*/README.md` +- **Reference product docs**: `docs/internal/product/features/` +- **Reference task specs**: `docs/internal/tasks/` \ No newline at end of file diff --git a/.claude/agents/typescript-expert/README.md b/.claude/agents/typescript-expert/README.md index 1c74ffb..3a31cfb 100644 --- a/.claude/agents/typescript-expert/README.md +++ b/.claude/agents/typescript-expert/README.md @@ -27,50 +27,139 @@ color: blue ### 1. Feature Implementation -- **Take ownership**: When a task is assigned, you implement it completely. +- **Take ownership**: When a task is assigned, implement it completely. - **Type System**: Design types that are safe, inferrable, and ergonomic. - **API Design**: Create clean function signatures with proper overloads. - **Method Chaining**: Ensure `.from()`, `.addNote()` return correctly narrowed types. -### 4. Testing +### 2. Testing - **Unit Tests**: Every feature needs unit tests (Vitest). - **Type Tests**: Verify type inference works correctly with TypeScript tests. - **Integration Tests**: Test the library in realistic scenarios. - **Edge Cases**: Test error cases, edge inputs, and boundary conditions. -### 5. PR Creation +### 3. PR Creation - **Complete PRs**: Implementation + tests + docs update. - **Clear Description**: Explain *why*, not just *what*. -- **Self-Review**: Review your own code before requesting review. +- **Self-Review**: Review your own code before requesting review. Use the Self-Review Checklist below. - **Address Feedback**: Respond to review comments and push fixes. -### 6. DX Advocacy +### 4. Documentation -- **Standard Schema Compliance**: Ensure interoperability with the TypeScript ecosystem. +- **Update Feature Docs**: Any public API change must update `docs/internal/product/features/`. +- **Run Doc Generation**: Execute `pnpm doc` to regenerate documentation. +- **Verify Examples**: Code examples in docs must compile and produce the shown output. +- **JSDoc Completeness**: All public exports require JSDoc comments. + +### 5. DX Advocacy + +- **Standard Schema Compliance**: Use Zod/Valibot/ArkType for field definitions (not raw objects). - **Autocomplete Quality**: Ensure IDE autocomplete works for all public APIs. -- **JSDoc Completeness**: Write comprehensive documentation in code. - **Migration Paths**: Make it easy to migrate from native errors. --- +## Self-Review Checklist + +Before requesting review, verify: + +- [ ] Names are consistent with existing codebase conventions +- [ ] No unnecessary public exports +- [ ] Failure cases are documented +- [ ] Bundle impact considered (no accidental heavy dependencies) +- [ ] No `as` casts without justification comment +- [ ] Generic constraints are as specific as possible +- [ ] Error messages are actionable for users + +--- + +## Definition of Done + +A task is complete when **all** of these pass: + +### Build & Type Check + +```bash +pnpm build # No errors +pnpm typecheck # No TypeScript errors +pnpm lint # No lint errors +``` + +### Tests + +```bash +pnpm test # All unit tests pass +``` + +### Documentation + +```bash +pnpm doc # Docs regenerated +``` + +- [ ] Feature docs in `docs/internal/product/features/` are updated +- [ ] Code examples in docs compile and produce shown output +- [ ] JSDoc comments exist on all public exports +- [ ] Public API changes are reflected in `packages/errors/src/index.ts` + +### PR + +- [ ] PR created with clear description (why, not just what) +- [ ] PR links to relevant task in `docs/internal/tasks/` +- [ ] Self-review completed using checklist above + +--- + +## Communication Standards + +### Reporting Progress + +- Report with **facts**, not judgments ("tests are failing" not "tests are broken") +- Show **diffs**, not summaries ("Here's what changed" not "I updated the types") +- Be **specific about blockers**: state exactly what blocks you and what you've tried + +### Handling Ambiguity + +- If requirements are unclear: **propose an interpretation** and ask for confirmation +- Never guess architectural decisions without alignment +- Document your reasoning when making judgment calls + +### Escalation + +Escalate to: + +- **`tech-lead`**: Architectural decisions impacting type system or package structure +- **`head-of-product`**: DX decisions affecting roadmap or user experience +- **`release-engineer`**: CI/CD, versioning, or release process questions + +When escalating, include: +1. What decision is needed +2. Options considered +3. Your recommendation with rationale + +--- + ## Type System Design Principles ### The Error Factory Pattern ```typescript -// User defines once +import { z } from 'zod'; + const ValidationError = error({ name: 'ValidationError', - fields: { field: { type: 'string' } }, - message: 'Field "{field}" is invalid', + fields: z.object({ + field: z.string(), + reason: z.string(), + }), + message: 'Field "{field}" is invalid: {reason}', }); // TypeScript infers: -// - Input type: { field: string } -// - Instance type: ValidationError & { fields: { field: string } } -// - Type guard: isValidationError(err) => err is ValidationError +// - Input type: { field: string; reason: string } +// - Instance type: ValidationError with fields { field: string; reason: string } ``` ### Inheritance Is Composable @@ -93,13 +182,8 @@ is(err, AppError); // true for DomainError, CombinedError, etc. ```typescript // Type guards enable TypeScript narrowing -if (isValidationError(err)) { - err.fields.field; // TypeScript knows this is string -} - -// Without narrowing, accessing fields should be a type error if (is(err, ValidationError)) { - err.fields; // TypeScript error: fields doesn't exist on unknown + err.fields.field; // TypeScript knows this exists } ``` @@ -111,67 +195,43 @@ if (is(err, ValidationError)) { ```typescript interface ErrorInstance { - name: string; // Always defined - message: string; // Always defined - stack: string; // Always defined + name: string; // Always defined + message: string; // Always defined + stack: string; // Always defined fields: Record; // Always defined (empty if none) - notes: string[]; // Always defined (empty if none) - cause: Error | null; // Always defined - causes: Error[]; // Always defined (may be empty) + notes: string[]; // Always defined (empty if none) + cause: Error | null; // Always defined + causes: Error[]; // Always defined (may be empty) context: Record | null; // Always defined - httpStatus: number | null; // Always defined + _factory: ErrorFactory; // Reference to the factory } ``` -### Field Definitions (Standard Schema) +### Error Factory Properties ```typescript -type FieldType = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'error' | 'unknown'; - -interface FieldDefinition { - type: FieldType; - required?: boolean; - items?: FieldDefinition; // For arrays -} -``` - -### Generic Constraints - -```typescript -// ErrorFactory: callable, returns ErrorInstance interface ErrorFactory> { - (fields?: Partial): ErrorInstance & { fields: TFields }; + (fields?: Partial): ErrorInstance; name: string; inherits?: ErrorFactory | ErrorFactory[]; + schema?: StandardSchemaV1; // Zod, Valibot, or ArkType schema + template?: string; // Original message template } - -// is() function with type narrowing -function is(error: unknown, ErrorType: ErrorFactory): error is ErrorInstance; ``` ---- - -## Escalation & Delegation (Sub-agents) - -When deep expertise is needed: - -- **`tech-lead`**: For architectural decisions that impact the type system or package structure. -- **`head-of-product`**: For DX decisions that affect the roadmap or user experience. - ---- +### Field Definitions (Standard Schema) -## Quality Gates +Use Zod/Valibot/ArkType. Example with Zod: -Before marking a task as complete: +```typescript +import { z } from 'zod'; -- [ ] Feature fully implemented -- [ ] Unit tests passing -- [ ] TypeScript strict mode passes -- [ ] No `any` types in public API surface -- [ ] IDE autocomplete works for all new APIs -- [ ] Type guard functions correctly narrow types -- [ ] JSDoc comments complete -- [ ] PR created with clear description +const schema = z.object({ + field: z.string(), + code: z.number().optional(), + details: z.record(z.unknown()), +}); +``` --- @@ -189,4 +249,5 @@ Before marking a task as complete: - **Check `CLAUDE.md`** for project-specific guidance - **Reference `tsconfig.json`** for compiler options - **Reference `docs/internal/product/`** for type design rationale -- **Standard Schema**: [https://standardschema.dev/](https://standardschema.dev/) \ No newline at end of file +- **Standard Schema**: [https://standardschema.dev/](https://standardschema.dev/) +- **Tasks**: `docs/internal/tasks/` for implementation roadmap \ No newline at end of file diff --git a/packages/errors/eslint.config.js b/packages/errors/eslint.config.js index ff7f4b3..cb77328 100644 --- a/packages/errors/eslint.config.js +++ b/packages/errors/eslint.config.js @@ -7,7 +7,10 @@ export default tseslint.config( { files: ['src/**/*.ts'], rules: { - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }], }, }, { diff --git a/packages/errors/package.json b/packages/errors/package.json index 36e177f..e6c8d3a 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -19,14 +19,23 @@ "type-check": "tsc --noEmit", "lint": "eslint src/" }, - "keywords": ["typescript", "errors", "exception", "error-handling"], + "keywords": [ + "typescript", + "errors", + "exception", + "error-handling" + ], "author": "Nesalia Inc. ", "license": "MIT", "devDependencies": { "@eslint/js": "^9.0.0", + "@types/node": "^25.9.1", "eslint": "^9.0.0", "typescript": "^6.0.3", "typescript-eslint": "^8.0.0", "vitest": "^4.1.7" + }, + "dependencies": { + "@standard-schema/spec": "^1.1.0" } } \ No newline at end of file diff --git a/packages/errors/src/error/capture.ts b/packages/errors/src/error/capture.ts new file mode 100644 index 0000000..0fbf211 --- /dev/null +++ b/packages/errors/src/error/capture.ts @@ -0,0 +1,40 @@ +/** + * Stack trace capture utilities. + */ + +import { STACK_FRAME_PATTERN } from './constants.js'; + +/** + * Captures the current stack trace, cleaning up internal frames. + * + * Uses Error.captureStackTrace in V8 environments for better performance. + * Falls back to string manipulation in other engines. + * + * @internal + */ +const captureStack = ( message: string ): string => { + const stack = new Error().stack || ''; + const lines = stack.split( '\n' ); + const cleanedLines: string[] = [ `Error: ${message}` ]; + + // Find start index (skip "Error: message" line) + let startIndex = 0; + for ( let i = 0; i < lines.length; i = i + 1 ) { + if ( STACK_FRAME_PATTERN.test( lines[i] ) ) { + startIndex = i; + break; + } + } + + // Filter internal frames + for ( let i = startIndex; i < lines.length; i = i + 1 ) { + const line = lines[i]; + if ( line.includes( 'node_modules/@deessejs' ) ) continue; + if ( line.includes( '__vite' ) ) continue; + cleanedLines.push( line ); + } + + return cleanedLines.join( '\n' ); +}; + +export { captureStack }; diff --git a/packages/errors/src/error/constants.ts b/packages/errors/src/error/constants.ts new file mode 100644 index 0000000..f00c4c7 --- /dev/null +++ b/packages/errors/src/error/constants.ts @@ -0,0 +1,9 @@ +/** + * Constants used throughout the error handling library. + */ + +// Regex pattern for matching stack frames +export const STACK_FRAME_PATTERN = /^\s+at\s+/i; + +// Template placeholder regex +export const TEMPLATE_PLACEHOLDER_REGEX = /\{(\w+)(?::(\w+))?\}/g; diff --git a/packages/errors/src/error/error.ts b/packages/errors/src/error/error.ts new file mode 100644 index 0000000..3b94cdf --- /dev/null +++ b/packages/errors/src/error/error.ts @@ -0,0 +1,126 @@ +/** + * @deessejs/errors - TypeScript Error Handling Library + * + * Error factory function and related implementations. + */ + +import type { StandardSchemaV1 } from '@standard-schema/spec'; + +import type { ErrorFactory, ErrorInstance } from './types.js'; +import { captureStack } from './capture.js'; +import { formatTemplate, hasTemplatePlaceholders } from './format.js'; + +// ============================================================================ +// Error Factory +// ============================================================================ + +/** + * Creates an error factory function for defining typed, structured errors. + * + * @param config - Error configuration + * @param config.name - Error name identifier + * @param config.fields - Standard Schema field definitions (Zod, Valibot, ArkType, etc.) + * @param config.inherits - Parent error factory to inherit from + * @param config.message - Message template with {field} placeholders + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const ValidationError = error({ + * name: 'ValidationError', + * fields: z.object({ + * field: z.string(), + * reason: z.string(), + * }), + * message: 'Field "{field}" is invalid: {reason}', + * }); + * + * const err = ValidationError({ field: 'email', reason: 'invalid format' }); + * // err.message === 'Field "email" is invalid: invalid format' + * ``` + * + * @example + * ```typescript + * // Single inheritance + * const AppError = error({ name: 'AppError' }); + * const ValidationError = error({ + * name: 'ValidationError', + * inherits: AppError, + * }); + * ``` + * + * @example + * ```typescript + * // Multiple inheritance + * const NetworkError = error({ name: 'NetworkError' }); + * const StorageError = error({ name: 'StorageError' }); + * const CombinedError = error({ + * name: 'CombinedError', + * inherits: [NetworkError, StorageError], + * }); + * ``` + */ +export const error = = Record>( + config: { + name: string; + fields?: StandardSchemaV1; + inherits?: ErrorFactory | ErrorFactory[]; + message?: string; + } +): ErrorFactory => { + const { name, fields, inherits, message } = config; + + /** + * Error factory function - creates error instances. + */ + const ErrorFactoryInstance = ( input?: Partial ): ErrorInstance => { + const fieldsData = ( input || {} ) as T; + + // Format message if template has placeholders + let errorMessage = name; + if ( message && hasTemplatePlaceholders( message ) ) { + errorMessage = formatTemplate( message, fieldsData ); + } else if ( message ) { + errorMessage = message; + } + + // Capture stack trace with cleaned frames + const stack = captureStack( errorMessage ); + + return { + name, + message: errorMessage, + stack, + fields: fieldsData, + notes: [], + cause: null, + causes: [], + context: null, + inherits: inherits ?? undefined, + _factory: ErrorFactoryInstance as ErrorFactory, + }; + }; + + // Attach metadata to the factory function + Object.defineProperty( ErrorFactoryInstance, 'name', { + value: name, + writable: false, + enumerable: false, + configurable: false, + } ); + + if ( inherits !== undefined ) { + ( ErrorFactoryInstance as ErrorFactory ).inherits = inherits; + } + + if ( fields !== undefined ) { + ( ErrorFactoryInstance as ErrorFactory ).schema = fields; + } + + if ( message !== undefined ) { + ( ErrorFactoryInstance as ErrorFactory ).rawMessage = message; + } + + return ErrorFactoryInstance as ErrorFactory; +}; diff --git a/packages/errors/src/error/format.ts b/packages/errors/src/error/format.ts new file mode 100644 index 0000000..6f54026 --- /dev/null +++ b/packages/errors/src/error/format.ts @@ -0,0 +1,61 @@ +/** + * Message formatting utilities. + */ + +import { TEMPLATE_PLACEHOLDER_REGEX } from './constants.js'; + +/** + * Extracts placeholder keys from a template string at compile-time. + * + * @example + * ```typescript + * type Keys = ExtractKeys<'Hello {name} and {age:number}'>; + * // => 'name' | 'age' + * ``` + */ +type ExtractKeys = + S extends `${string}{${infer Key}}${infer Rest}` + ? (Key extends `${infer RealKey}:${string}` ? RealKey : Key) | ExtractKeys + : never; + +/** + * Formats a message template by replacing {field} placeholders with values. + * Data keys are validated at compile-time based on template string. + * + * @internal + */ +const formatTemplate = ( + template: S, + data: Record, unknown> +): string => { + return template.replace( /\{(\w+)(?::(\w+))?\}/g, ( fullMatch, fieldName, modifier ) => { + const value = data[fieldName as keyof typeof data]; + if ( value === undefined ) { + return fullMatch; + } + + if ( modifier === 'upper' ) { + return String( value ).toUpperCase(); + } + if ( modifier === 'lower' ) { + return String( value ).toLowerCase(); + } + if ( modifier === 'json' ) { + return JSON.stringify( value ); + } + + return String( value ); + } ); +}; + +/** + * Checks if a message string contains template placeholders. + * + * @internal + */ +const hasTemplatePlaceholders = ( message: string ): boolean => { + TEMPLATE_PLACEHOLDER_REGEX.lastIndex = 0; + return TEMPLATE_PLACEHOLDER_REGEX.test( message ); +}; + +export { formatTemplate, hasTemplatePlaceholders }; diff --git a/packages/errors/src/error/types.ts b/packages/errors/src/error/types.ts new file mode 100644 index 0000000..5f8f8cb --- /dev/null +++ b/packages/errors/src/error/types.ts @@ -0,0 +1,77 @@ +/** + * Error factory types. + */ + +import type { StandardSchemaV1 } from '@standard-schema/spec'; + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Core properties present on every error instance. + * These are guaranteed to exist regardless of how the error was created. + */ +export type ErrorInstanceCore = { + /** Error name identifier */ + name: string; + /** Human-readable error message */ + message: string; + /** Stack trace string */ + stack: string; +}; + +/** + * Error factory function type. + * Creates typed, structured errors with optional field definitions. + */ +export type ErrorFactory = Record> = { + ( fields?: Partial ): ErrorInstance; + name: string; + inherits?: ErrorFactory | ErrorFactory[]; + schema?: StandardSchemaV1; + rawMessage?: string; +}; + +/** + * Error instance returned by an ErrorFactory. + * Contains all standard Error properties plus additional domain-specific fields. + * + * Note: Methods like .addNote() and .from() are implemented in separate tasks. + */ +export type ErrorInstance = Record> = + ErrorInstanceCore & { + /** User-defined fields from Standard Schema */ + fields: TFields; + // TODO: Implement .addNote() method (Task 05) + /** Additional notes added via .addNote() */ + notes: string[]; + // TODO: Implement .from() method (Task 06) + /** Direct cause of this error (from .from()) */ + cause: Error | null; + /** Full cause chain from .from() calls */ + causes: Error[]; + // TODO: Implement context injection (Task 10) + /** Injected context data */ + context: Record | null; + /** Parent error factories for type checking */ + inherits?: ErrorFactory | ErrorFactory[]; + /** Reference to the factory that created this instance */ + _factory: ErrorFactory; + }; + +/** + * Full error config for the error() function. + * + * @internal - Type parameter reserved for future Standard Schema type inference + */ +export type ErrorConfig<_T extends Record = Record> = { + /** Error name identifier */ + name: string; + /** Standard Schema field definitions */ + fields?: StandardSchemaV1; + /** Single parent error factory to inherit from */ + inherits?: ErrorFactory | ErrorFactory[]; + /** Message template with {field} placeholders */ + message?: string; +}; diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts deleted file mode 100644 index 4815946..0000000 --- a/packages/errors/src/index.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { helloWorld } from '../src/index'; - -describe('helloWorld', () => { - it('should return "Hello, World!"', () => { - expect(helloWorld()).toBe('Hello, World!'); - }); -}); \ No newline at end of file diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index c3fa6d7..19966dc 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,3 +1,16 @@ -export function helloWorld(): string { - return 'Hello, World!'; -} \ No newline at end of file +/** + * @deessejs/errors - TypeScript Error Handling Library + * + * Public API exports for the error handling library. + */ + +// Types +export type { StandardSchemaV1 } from '@standard-schema/spec'; +export type { + ErrorFactory, + ErrorInstance, + ErrorInstanceCore, +} from './error/types.js'; + +// Error factory function +export { error } from './error/error.js'; diff --git a/packages/errors/tests/error.test.ts b/packages/errors/tests/error.test.ts new file mode 100644 index 0000000..100e177 --- /dev/null +++ b/packages/errors/tests/error.test.ts @@ -0,0 +1,344 @@ +/** + * Unit tests for the error() factory function. + */ + +import { describe, it, expect } from 'vitest'; +import { error } from '../src/error/error.js'; +import type { ErrorFactory, ErrorInstance, StandardSchemaV1 } from '../src/index.js'; + +// Mock Standard Schema interface for testing (simplified StandardSchemaV1) +const createMockSchema = ( name = 'mock' ): StandardSchemaV1 => { + return { + '~standard': { + version: 1, + vendor: name, + validate: () => ( { value: undefined as unknown as T } ), + }, + }; +}; + +describe( 'error() factory function', () => { + describe( 'basic usage', () => { + it( 'should create an error factory with only a name', () => { + const NotFoundError = error( { + name: 'NotFoundError', + } ); + + expect( typeof NotFoundError ).toBe( 'function' ); + expect( NotFoundError.name ).toBe( 'NotFoundError' ); + } ); + + it( 'should create an error factory with name property', () => { + const AppError = error( { name: 'AppError' } ); + + expect( AppError.name ).toBe( 'AppError' ); + } ); + + it( 'should create error instance with name as message when no template', () => { + const BasicError = error( { name: 'BasicError' } ); + + const instance = BasicError(); + expect( instance ).toBeDefined(); + expect( instance.name ).toBe( 'BasicError' ); + expect( instance.message ).toBe( 'BasicError' ); + expect( instance.stack ).toBeDefined(); + expect( instance.stack ).toContain( 'Error: BasicError' ); + } ); + } ); + + describe( 'ErrorInstance properties', () => { + it( 'should create error instance with all required properties', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + // Core properties + expect( typeof instance.name ).toBe( 'string' ); + expect( typeof instance.message ).toBe( 'string' ); + expect( typeof instance.stack ).toBe( 'string' ); + + // Additional properties + expect( instance.fields ).toBeDefined(); + expect( typeof instance.fields ).toBe( 'object' ); + expect( Array.isArray( instance.notes ) ).toBe( true ); + expect( instance.notes ).toEqual( [] ); + expect( instance.cause ).toBeNull(); + expect( Array.isArray( instance.causes ) ).toBe( true ); + expect( instance.causes ).toEqual( [] ); + expect( instance.context ).toBeNull(); + } ); + + it( 'should have _factory reference back to the creator', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( instance._factory ).toBe( TestError ); + } ); + + it( 'should have inherits reference when inheriting', () => { + const ParentError = error( { name: 'ParentError' } ); + const ChildError = error( { + name: 'ChildError', + inherits: ParentError, + } ); + const instance = ChildError(); + + expect( instance.inherits ).toBe( ParentError ); + } ); + } ); + + describe( 'inherits option', () => { + it( 'should support single inheritance', () => { + const AppError = error( { name: 'AppError' } ); + const ValidationError = error( { + name: 'ValidationError', + inherits: AppError, + } ); + + expect( ValidationError.inherits ).toBe( AppError ); + } ); + + it( 'should support multiple inheritance', () => { + const NetworkError = error( { name: 'NetworkError' } ); + const StorageError = error( { name: 'StorageError' } ); + const CombinedError = error( { + name: 'CombinedError', + inherits: [NetworkError, StorageError], + } ); + + expect( Array.isArray( CombinedError.inherits ) ).toBe( true ); + expect( ( CombinedError.inherits as ErrorFactory[] ).length ).toBe( 2 ); + } ); + + it( 'should not have inherits property when not specified', () => { + const SimpleError = error( { name: 'SimpleError' } ); + + expect( 'inherits' in SimpleError ).toBe( false ); + } ); + + it( 'should store inherits on factory for later type checking', () => { + const ParentA = error( { name: 'ParentA' } ); + const ParentB = error( { name: 'ParentB' } ); + const Child = error( { + name: 'Child', + inherits: [ParentA, ParentB], + } ); + + const instance = Child(); + expect( instance.inherits ).toBeDefined(); + expect( Array.isArray( instance.inherits ) ).toBe( true ); + } ); + } ); + + describe( 'message template', () => { + it( 'should format message with field placeholders', () => { + const ValidationError = error<{ field: string }>( { + name: 'ValidationError', + message: 'Field "{field}" is invalid', + } ); + + const instance = ValidationError( { field: 'email' } ); + expect( instance.message ).toBe( 'Field "email" is invalid' ); + } ); + + it( 'should handle multiple placeholders', () => { + const ValidationError = error<{ field: string; expected: string; actual: string }>( { + name: 'ValidationError', + message: 'Field "{field}" expected {expected}, got {actual}', + } ); + + const instance = ValidationError( { + field: 'age', + expected: 'number', + actual: 'string', + } ); + expect( instance.message ).toBe( 'Field "age" expected number, got string' ); + } ); + + it( 'should use name as default message when no template', () => { + const InternalError = error( { name: 'InternalError' } ); + + const instance = InternalError(); + expect( instance.message ).toBe( 'InternalError' ); + } ); + + it( 'should support :upper modifier', () => { + const ErrorWithModifier = error<{ userId: string }>( { + name: 'ErrorWithModifier', + message: 'User ID: {userId:upper}', + } ); + + const instance = ErrorWithModifier( { userId: 'abc123' } ); + expect( instance.message ).toBe( 'User ID: ABC123' ); + } ); + + it( 'should support :lower modifier', () => { + const ErrorWithModifier = error<{ msg: string }>( { + name: 'ErrorWithModifier', + message: 'Message: {msg:lower}', + } ); + + const instance = ErrorWithModifier( { msg: 'HELLO WORLD' } ); + expect( instance.message ).toBe( 'Message: hello world' ); + } ); + + it( 'should support :json modifier', () => { + const DataError = error<{ data: { id: number; name: string } }>( { + name: 'DataError', + message: 'Invalid data: {data:json}', + } ); + + const instance = DataError( { data: { id: 1, name: 'test' } } ); + expect( instance.message ).toBe( 'Invalid data: {"id":1,"name":"test"}' ); + } ); + + it( 'should leave placeholder unchanged if field not found', () => { + const PartialError = error<{ field: string }>( { + name: 'PartialError', + message: 'Field "{field}" is invalid', + } ); + + const instance = PartialError( { field: '' } ); + expect( instance.message ).toBe( 'Field "" is invalid' ); + } ); + + it( 'should format template even with no fields provided', () => { + const TemplateError = error<{ field: string }>( { + name: 'TemplateError', + message: 'Field "{field}" is invalid', + } ); + + const instance = TemplateError(); + expect( instance.message ).toBe( 'Field "{field}" is invalid' ); + } ); + + it( 'should not format message without placeholders', () => { + const FixedError = error( { + name: 'FixedError', + message: 'Something went wrong', + } ); + + const instance = FixedError(); + expect( instance.message ).toBe( 'Something went wrong' ); + } ); + } ); + + describe( 'fields with Standard Schema', () => { + it( 'should accept Standard Schema fields', () => { + const mockSchema = createMockSchema<{ field: string; reason: string }>(); + + const ValidationError = error( { + name: 'ValidationError', + fields: mockSchema, + } ); + + expect( ValidationError.schema ).toBeDefined(); + } ); + + it( 'should store fields schema for runtime validation', () => { + const mockSchema = createMockSchema<{ field: string }>(); + + const FieldError = error( { + name: 'FieldError', + fields: mockSchema, + } ); + + expect( FieldError.schema ).toBeDefined(); + } ); + + it( 'should return empty fields object by default', () => { + const NoFieldsError = error( { name: 'NoFieldsError' } ); + + const instance = NoFieldsError(); + expect( instance.fields ).toEqual( {} ); + } ); + } ); + + describe( 'type inference', () => { + it( 'should infer proper types for ErrorFactory', () => { + const AppError = error( { name: 'AppError' } ); + + // Type checks - these compile if types are correct + const instance: ErrorInstance = AppError(); + expect( instance.name ).toBe( 'AppError' ); + } ); + + it( 'should allow passing fields to factory with typed error', () => { + const FieldError = error<{ field: string }>( { + name: 'FieldError', + message: 'Field "{field}" is invalid', + } ); + + // Should accept partial fields + const instance = FieldError( { field: 'test' } ); + expect( instance.fields.field ).toBe( 'test' ); + } ); + + it( 'should work with typed config', () => { + type Config = { field: string }; + + const TypedError = error( { + name: 'TypedError', + message: 'Field "{field}" is missing', + } ); + + // Instance should have field + const instance = TypedError( { field: 'email' } ); + expect( instance.fields.field ).toBe( 'email' ); + } ); + + it( 'should infer empty fields when no type provided', () => { + const NoFieldsError = error( { name: 'NoFieldsError' } ); + const instance = NoFieldsError(); + + // fields should be Record which is empty + expect( instance.fields ).toEqual( {} ); + } ); + } ); + + describe( 'factory identity', () => { + it( 'should create unique factory instances', () => { + const ErrorA = error( { name: 'ErrorA' } ); + const ErrorB = error( { name: 'ErrorB' } ); + + expect( ErrorA ).not.toBe( ErrorB ); + expect( ErrorA.name ).not.toBe( ErrorB.name ); + } ); + + it( 'should maintain factory reference on instances', () => { + const TestError = error( { name: 'TestError' } ); + const instance1 = TestError(); + const instance2 = TestError(); + + expect( instance1._factory ).toBe( TestError ); + expect( instance2._factory ).toBe( TestError ); + expect( instance1._factory ).toBe( instance2._factory ); + } ); + } ); + + describe( 'stack trace', () => { + it( 'should generate a stack trace', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( instance.stack ).toBeDefined(); + expect( instance.stack.length ).toBeGreaterThan( 0 ); + } ); + + it( 'should include error name in stack', () => { + const TestError = error( { name: 'TestError' } ); + const instance = TestError(); + + expect( instance.stack ).not.toBe( '' ); + } ); + + it( 'should include formatted message in stack', () => { + const TestError = error<{ field: string }>( { + name: 'TestError', + message: 'Custom message for {field}', + } ); + const instance = TestError( { field: 'value' } ); + + expect( instance.stack ).toContain( 'Custom message for value' ); + } ); + } ); +} ); diff --git a/packages/errors/tsconfig.build.json b/packages/errors/tsconfig.build.json index 3257ca7..9cd3db8 100644 --- a/packages/errors/tsconfig.build.json +++ b/packages/errors/tsconfig.build.json @@ -8,5 +8,5 @@ "rootDir": "./src" }, "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "tests/**/*.ts"] } \ No newline at end of file diff --git a/packages/errors/vitest.config.ts b/packages/errors/vitest.config.ts index 7192c61..903590d 100644 --- a/packages/errors/vitest.config.ts +++ b/packages/errors/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - environment: 'node' + environment: 'node', + include: ['tests/**/*.ts'], } }); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8240785..01426d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,11 +73,18 @@ importers: specifier: ^6.0.3 version: 6.0.3 - packages/example: + packages/errors: + dependencies: + '@standard-schema/spec': + specifier: ^1.1.0 + version: 1.1.0 devDependencies: '@eslint/js': specifier: ^9.0.0 version: 9.39.4 + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 eslint: specifier: ^9.0.0 version: 9.39.4(jiti@2.7.0)