intellerror 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,74 +11,91 @@
11
11
  Standard error logs are noisy, cluttered with `node_modules`, and lack context. **IntellError** gives you the "Why" and "How to Fix" instantly.
12
12
 
13
13
  - ❌ **Clean Stack Traces**: Auto-hides Node.js internals and collapses `node_modules` noise.
14
- - 📍 **Smart Highlighting**: Identifies exactly where the error occurred in *your* code.
15
- - 💡 **Actionable Suggestions**: 50+ built-in rules for React, Express, Node.js, and Browser APIs.
16
- - 📸 **Code Snapshots**: Sees 3 lines of source code directly in your terminal (Node.js only).
17
- - 🔍 **One-Click Troubleshooting**: Instant search links for Google, StackOverflow, and GitHub.
18
- - 🌓 **Intelligent Warnings**: Not just for errors! Also formats `console.warn()` with suggestions.
19
- - 🌐 **Universal Styling**: Beautiful ANSI colors for Terminal and native `%c` CSS for Browsers.
20
- - 📢 **Team Integration**: Push critical dev errors directly to **Slack** or **Discord** webhooks.
14
+ - 📍 **Source Map Support**: Auto-translates minified/compiled code back to original TypeScript/Source locations.
15
+ - 🔗 **Async Stack Chaining**: Recursively traverses `Error.cause` to show the full async story.
16
+ - 💡 **Actionable Suggestions 2.0**: 100+ context-aware rules with confidence scoring.
17
+ - 📸 **Code Snapshots**: See the exact line of code directly in your terminal (Node.js only).
18
+ - 📊 **Observability Ready**: Includes **JSON Mode**, **Error Fingerprinting**, and **Severity Levels**.
19
+ - 🌐 **Universal Runtime Support**: Native support for **Node.js**, **Bun**, **Deno**, and **Edge/Serverless**.
21
20
 
22
21
  ---
23
22
 
24
23
  ## 🚀 Quick Start (Zero-Config)
25
24
 
26
- You can set up `IntellError` globally to catch all unhandled issues across your entire project with just one line.
25
+ Add this at the very top of your entry file to catch all unhandled errors automatically.
27
26
 
28
27
  ### For Browser (React, Vite, Next.js)
29
- Add this at the very top of your entry file (e.g., `main.tsx` or `index.tsx`):
30
-
31
28
  ```typescript
32
29
  import 'intellerror/register';
33
30
  ```
34
31
 
35
- ### For Node.js (CLI)
36
- Run your application with the register hook:
37
-
32
+ ### For Node.js / Bun / Deno
38
33
  ```bash
39
- # ESM (Node.js 20+)
34
+ # Node.js 20+
40
35
  node --import intellerror/register index.js
41
36
 
42
- # CommonJS
43
- node -r intellerror/register index.js
37
+ # Bun
38
+ bun index.ts # (Auto-detected if you import it)
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🛠️ Advanced Features
44
+
45
+ ### 🤖 Machine-Readable JSON Mode
46
+ Enable production logging that flows perfectly into Datadog, Splunk, or Loki.
47
+ ```typescript
48
+ import { formatErrorJSON } from 'intellerror';
49
+
50
+ try {
51
+ // your code
52
+ } catch (err) {
53
+ console.log(formatErrorJSON(err));
54
+ }
55
+ ```
56
+
57
+ ### 🔗 Error Chaining (`Error.cause`)
58
+ Never lose context again. IntellError recursively handles nested errors.
59
+ ```text
60
+ ERROR Failed to fetch user from DB
61
+ 2026-04-06T06:07:49.123Z | uptime: 12.5s | ID: a1b2c3d4
62
+
63
+ 🔗 Causes:
64
+ caused by: Error: Connection refused (timeout after 5000ms)
65
+ ```
66
+
67
+ ### 📍 Source Map Integration
68
+ If a `.map` file exists alongside your compiled script, IntellError will automatically show the original source location.
69
+ ```text
70
+ 📍 Location:
71
+ src/users/service.ts:45:10 [mapped] ← YOUR CODE
44
72
  ```
45
73
 
46
74
  ---
47
75
 
48
- ## ⚙️ Configuration & Customization
76
+ ## ⚙️ Configuration
49
77
 
50
78
  Fine-tune the output or add your own project-specific intelligence.
51
79
 
52
80
  ```typescript
53
- import { setupConfig, registerRule } from 'intellerror';
81
+ import { setupConfig } from 'intellerror';
54
82
 
55
83
  setupConfig({
56
84
  showNodeModules: false, // Hide noise?
57
85
  showNodeInternals: false, // Hide node internals?
58
86
  suggestionsEnabled: true, // Show the "💡 Suggestions"?
59
- interceptWarnings: true, // Also format console.warn?
60
- showSearchLinks: true, // Show troubleshooting links?
87
+ sourceMapsEnabled: true, // Enable original source mapping?
61
88
  webhookUrl: 'https://...' // Push to Slack/Discord?
62
89
  });
63
-
64
- // Add your own "Thinking" to the engine
65
- registerRule({
66
- match: (err) => err.message.includes("AUTH_FAILED"),
67
- message: "Authentication failure detected.",
68
- fix: "Ensure your .env contains a valid API_KEY.",
69
- description: "The request was rejected by the server due to invalid credentials."
70
- });
71
90
  ```
72
91
 
73
92
  ---
74
93
 
75
94
  ## 📖 Manual Usage
76
95
 
77
- If you prefer to format specific errors manually in your `try-catch` blocks:
78
-
79
96
  ### Terminal / Node.js
80
97
  ```typescript
81
- import { formatError, formatWarning } from 'intellerror';
98
+ import { formatError } from 'intellerror';
82
99
 
83
100
  try {
84
101
  // your code...
@@ -92,9 +109,9 @@ try {
92
109
  import { formatErrorBrowser } from 'intellerror';
93
110
 
94
111
  try {
95
- // your React code...
112
+ // ...
96
113
  } catch (err) {
97
- // Note: MUST use the spread operator (...) for CSS styling
114
+ // MUST use the spread operator (...) for CSS styling
98
115
  console.log(...formatErrorBrowser(err));
99
116
  }
100
117
  ```
@@ -109,56 +126,41 @@ import express from 'express';
109
126
  import { errorFormatter } from 'intellerror';
110
127
 
111
128
  const app = express();
112
- // ... routes ...
113
-
114
- // Add at the very end of your middleware stack
115
129
  app.use(errorFormatter());
116
130
  ```
117
131
 
118
132
  ---
119
133
 
120
- ## 🎮 Demos & Examples
121
-
122
- We've included several ready-to-run examples in the `examples/` directory:
123
-
124
- - **Basic Error**: `npm run example:basic`
125
- - **Async Errors**: `npm run example:async`
126
- - **Custom Errors**: `npm run example:custom`
127
- - **Run All**: `npm run examples:all`
128
-
129
- ### Run Unit Tests
130
- ```bash
131
- npm run test:unit
132
- ```
133
-
134
134
  ---
135
135
 
136
- ## Example Output (Terminal)
136
+ ## 🌍 Environment Variables (Zero-Config)
137
+ You can configure IntellError without changing a single line of code using environment variables.
137
138
 
138
- ```text
139
- TypeError Cannot read properties of undefined (reading 'name')
139
+ | Variable | Default | Description |
140
+ | :--- | :--- | :--- |
141
+ | `INTELLERROR_JSON` | `false` | Enable structured JSON output for all unhandled errors. |
142
+ | `INTELLERROR_SOURCE_MAPS` | `true` | Enable/disable original source mapping via `.map` files. |
143
+ | `INTELLERROR_SUGGESTIONS` | `true` | Show the "💡 Suggestions" section. |
144
+ | `INTELLERROR_SHOW_MODULES` | `false` | Show `node_modules` frames in the stack trace. |
145
+ | `INTELLERROR_SHOW_INTERNALS` | `false` | Show Node.js internal frames in the stack trace. |
146
+ | `INTELLERROR_SEARCH_LINKS` | `true` | Show troubleshooting links. |
140
147
 
141
- 📍 Location:
142
- src/index.ts:12:15 ← YOUR CODE
148
+ ---
143
149
 
144
- > 11 | const user = await fetchUser();
145
- > 12 | console.log(user.name);
146
- > 13 | }
150
+ ## 🎮 Examples
151
+ Explore the `examples/` directory for ready-to-run demos:
152
+ - `npm run example:basic`
153
+ - `npm run example:async`
154
+ - `npm run example:advanced` (New: Chaining & JSON mode)
147
155
 
148
- 💡 Suggestions:
149
- • Accessing property on undefined object.
150
- Fix: Use optional chaining like 'obj?.prop' or ensure the object is initialized.
156
+ ---
151
157
 
152
- 🔍 Troubleshoot:
153
- Google: https://google.com/...
154
- • GitHub: https://github.com/...
158
+ ## 📄 License
159
+ MIT © [darshan1005](https://github.com/darshan1005)
155
160
 
156
- 📦 Stack:
157
- → src/index.ts:12:15
158
- → src/server.ts:45:10
159
- (4 node internals and 2 node_modules hidden)
160
- ```
161
+ ---
161
162
 
162
- ## 📄 License
163
+ ## 🤝 Contributing
164
+ IntellError is open for contributions! Whether it's adding new suggestion rules, improving the parser, or adding more framework integrations, feel free to open a PR or Issue on [GitHub](https://github.com/darshan1005/IntellError).
163
165
 
164
- ISC © [darshan1005](https://github.com/darshan1005)
166
+ *Built with humans in mind.*
@@ -0,0 +1,28 @@
1
+ import { formatError, formatErrorJSON, setupConfig } from '../src/index.js';
2
+
3
+ // Example 1: Async Chaining with Error.cause
4
+ async function databaseCall() {
5
+ throw new Error('Connection refused (timeout after 5000ms)');
6
+ }
7
+
8
+ async function fetchUser() {
9
+ try {
10
+ await databaseCall();
11
+ } catch (err) {
12
+ throw new Error('Failed to fetch user from DB', { cause: err });
13
+ }
14
+ }
15
+
16
+ async function main() {
17
+ console.log('--- 🔗 Async Chaining Example ---');
18
+ try {
19
+ await fetchUser();
20
+ } catch (err) {
21
+ console.log(formatError(err));
22
+
23
+ console.log('\n--- 🤖 Machine-Readable JSON Mode ---');
24
+ console.log(formatErrorJSON(err));
25
+ }
26
+ }
27
+
28
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intellerror",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Intelligent error formatting for Node.js and Browser. Transforms ugly stack traces into clean, readable, and actionable output with smart code suggestions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,8 @@
24
24
  "example:basic": "tsx examples/basic-usage.ts",
25
25
  "example:async": "tsx examples/async-error.ts",
26
26
  "example:custom": "tsx examples/custom-error.ts",
27
- "examples:all": "npm run example:basic && npm run example:async && npm run example:custom"
27
+ "example:advanced": "tsx examples/advanced-usage.ts",
28
+ "examples:all": "npm run example:basic && npm run example:async && npm run example:custom && npm run example:advanced"
28
29
  },
29
30
  "repository": {
30
31
  "type": "git",
package/src/config.ts CHANGED
@@ -4,15 +4,21 @@ export interface IntellErrorConfig {
4
4
  suggestionsEnabled: boolean;
5
5
  interceptWarnings: boolean;
6
6
  showSearchLinks: boolean;
7
+ sourceMapsEnabled: boolean;
8
+ useJsonMode: boolean; // NEW: Should registration use JSON formatting by default?
7
9
  webhookUrl?: string;
8
10
  }
9
11
 
12
+ const env = typeof process !== 'undefined' ? process.env : {};
13
+
10
14
  let config: IntellErrorConfig = {
11
- showNodeModules: false,
12
- showNodeInternals: false,
13
- suggestionsEnabled: true,
14
- interceptWarnings: false,
15
- showSearchLinks: true,
15
+ showNodeModules: env.INTELLERROR_SHOW_MODULES === 'true',
16
+ showNodeInternals: env.INTELLERROR_SHOW_INTERNALS === 'true',
17
+ suggestionsEnabled: env.INTELLERROR_SUGGESTIONS !== 'false',
18
+ interceptWarnings: env.INTELLERROR_INTERCEPT_WARNINGS === 'true',
19
+ showSearchLinks: env.INTELLERROR_SEARCH_LINKS !== 'false',
20
+ sourceMapsEnabled: env.INTELLERROR_SOURCE_MAPS !== 'false',
21
+ useJsonMode: env.INTELLERROR_JSON === 'true',
16
22
  };
17
23
 
18
24
  /**
@@ -1,4 +1,4 @@
1
- import { parseStack } from '../parser/index.js';
1
+ import { parseStack, getErrorChain } from '../parser/index.js';
2
2
  import { getSuggestions } from '../suggestions/index.js';
3
3
  import { getConfig } from '../config.js';
4
4
  import { getSearchLinks } from '../suggestions/links.js';
@@ -13,7 +13,9 @@ export function formatErrorBrowser(error: Error | unknown): any[] {
13
13
  }
14
14
 
15
15
  const config = getConfig();
16
- const stackFrames = parseStack(error);
16
+ const errorChain = getErrorChain(error);
17
+ const topError = errorChain[0];
18
+ const stackFrames = topError.frames;
17
19
  const userFrame = stackFrames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
18
20
 
19
21
  const errorType = error.constructor.name || 'Error';
@@ -28,7 +30,12 @@ export function formatErrorBrowser(error: Error | unknown): any[] {
28
30
  text += `%c📍 Location:%c\n`;
29
31
  styles.push('font-weight: bold;', '');
30
32
 
31
- text += `%c${userFrame.file}:${userFrame.lineNumber}:${userFrame.column}%c (YOUR CODE)\n\n`;
33
+ const fileName = userFrame.originalFile || userFrame.file;
34
+ const line = userFrame.originalLineNumber || userFrame.lineNumber;
35
+ const col = userFrame.originalColumn || userFrame.column;
36
+ const isMapped = userFrame.originalFile ? ' [mapped]' : '';
37
+
38
+ text += `%c${fileName}:${line}:${col}%c${isMapped} (YOUR CODE)\n\n`;
32
39
  styles.push('color: #1890ff; text-decoration: underline;', 'color: grey; font-style: italic;');
33
40
  }
34
41
 
@@ -1,32 +1,59 @@
1
1
  import chalk from 'chalk';
2
- import { parseStack } from '../parser/index.js';
2
+ import * as crypto from 'node:crypto';
3
+ import { parseStack, getErrorChain, type ParsedStackFrame, type ErrorChain } from '../parser/index.js';
3
4
  import { getSuggestions } from '../suggestions/index.js';
4
5
  import { getConfig } from '../config.js';
5
6
  import { getCodeSnapshot } from './snapshot.js';
6
7
  import { getSearchLinks } from '../suggestions/links.js';
7
8
 
9
+ function getFingerprint(type: string, frame: ParsedStackFrame | undefined): string {
10
+ const frameId = frame ? `${frame.file}:${frame.lineNumber}:${frame.column}` : 'no-frame';
11
+ return crypto.createHash('sha1').update(`${type}:${frameId}`).digest('hex').substring(0, 8);
12
+ }
13
+
8
14
  export function formatError(error: Error | unknown): string {
9
15
  if (!(error instanceof Error)) {
10
16
  return String(error);
11
17
  }
12
18
 
13
19
  const config = getConfig();
14
- const stackFrames = parseStack(error);
20
+ const errorChain = getErrorChain(error);
21
+ const topError = errorChain[0];
22
+ const stackFrames = topError.frames;
15
23
 
16
24
  // Find first user frame that is not node internal or node_modules
17
25
  const userFrame = stackFrames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
18
26
 
19
27
  const errorType = error.constructor.name || 'Error';
20
- const typeLabel = chalk.bgRed.white(` ${errorType} `);
21
- let output = `\n${typeLabel} ${chalk.red(error.message)}\n\n`;
28
+ const severity = (error as any).severity || 'ERROR';
29
+
30
+ const severityColors: Record<string, any> = {
31
+ 'FATAL': chalk.bgRed.white.bold,
32
+ 'ERROR': chalk.bgRed.white,
33
+ 'WARN': chalk.bgYellow.black,
34
+ 'INFO': chalk.bgBlue.white
35
+ };
36
+
37
+ const typeLabel = (severityColors[severity] || severityColors['ERROR'])(` ${severity} `);
38
+ const timestamp = new Date().toISOString();
39
+ const uptime = process.uptime().toFixed(1);
40
+ const fingerprint = getFingerprint(errorType, userFrame);
41
+
42
+ let output = `\n${typeLabel} ${chalk.red(`${errorType}: ${error.message}`)}\n`;
43
+ output += chalk.dim(`${timestamp} | uptime: ${uptime}s | ID: ${fingerprint}\n\n`);
22
44
 
23
45
  if (userFrame && userFrame.file) {
24
46
  output += `${chalk.bold('📍 Location:')}\n`;
25
- output += `${chalk.cyan(userFrame.file)}:${chalk.yellow(userFrame.lineNumber)}:${chalk.yellow(userFrame.column)} ${chalk.gray('← YOUR CODE')}\n\n`;
47
+ const fileName = userFrame.originalFile || userFrame.file;
48
+ const line = userFrame.originalLineNumber || userFrame.lineNumber;
49
+ const col = userFrame.originalColumn || userFrame.column;
50
+ const isMapped = userFrame.originalFile ? chalk.magenta(' [mapped]') : '';
51
+
52
+ output += `${chalk.cyan(fileName)}:${chalk.yellow(line)}:${chalk.yellow(col)}${isMapped} ${chalk.gray('← YOUR CODE')}\n\n`;
26
53
 
27
54
  // Add code snapshot (Node only)
28
- if (userFrame.lineNumber && userFrame.file) {
29
- output += getCodeSnapshot(userFrame.file, userFrame.lineNumber);
55
+ if (line && fileName && !userFrame.originalFile) { // Snapshot only for actual files on disk
56
+ output += getCodeSnapshot(fileName, line);
30
57
  }
31
58
  }
32
59
 
@@ -53,6 +80,15 @@ export function formatError(error: Error | unknown): string {
53
80
  output += `• ${chalk.dim('GitHub:')} ${chalk.blue(searchLinks[2])}\n\n`;
54
81
  }
55
82
 
83
+ // Handle causes
84
+ if (errorChain.length > 1) {
85
+ output += `${chalk.bold('🔗 Causes:')}\n`;
86
+ for (let i = 1; i < errorChain.length; i++) {
87
+ output += `${' '.repeat(i)}caused by: ${chalk.red(errorChain[i].error.constructor.name)}: ${errorChain[i].error.message}\n`;
88
+ }
89
+ output += '\n';
90
+ }
91
+
56
92
  output += `${chalk.bold('📦 Stack:')}\n`;
57
93
 
58
94
  let hiddenInternalsCount = 0;
@@ -69,8 +105,13 @@ export function formatError(error: Error | unknown): string {
69
105
  continue;
70
106
  }
71
107
 
72
- const fileStr = frame.file
73
- ? `${chalk.cyan(frame.file)}:${chalk.yellow(frame.lineNumber)}:${chalk.yellow(frame.column)}`
108
+ const fileName = frame.originalFile || frame.file;
109
+ const line = frame.originalLineNumber || frame.lineNumber;
110
+ const col = frame.originalColumn || frame.column;
111
+ const isMapped = frame.originalFile ? chalk.magenta('*') : '';
112
+
113
+ const fileStr = fileName
114
+ ? `${chalk.cyan(fileName)}:${chalk.yellow(line)}:${chalk.yellow(col)}${isMapped}`
74
115
  : chalk.gray('<unknown>');
75
116
 
76
117
  output += `→ ${fileStr}\n`;
@@ -86,6 +127,40 @@ export function formatError(error: Error | unknown): string {
86
127
  return output;
87
128
  }
88
129
 
130
+ export function formatErrorJSON(error: Error | unknown): string {
131
+ if (!(error instanceof Error)) {
132
+ return JSON.stringify({ error: String(error) });
133
+ }
134
+
135
+ const config = getConfig();
136
+ const errorChain = getErrorChain(error);
137
+ const topError = errorChain[0];
138
+ const userFrame = topError.frames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
139
+
140
+ return JSON.stringify({
141
+ timestamp: new Date().toISOString(),
142
+ uptime: process.uptime(),
143
+ severity: (error as any).severity || 'ERROR',
144
+ fingerprint: getFingerprint(error.constructor.name, userFrame),
145
+ error: {
146
+ type: error.constructor.name,
147
+ message: error.message,
148
+ stack: topError.frames,
149
+ causes: errorChain.slice(1).map(c => ({
150
+ type: c.error.constructor.name,
151
+ message: c.error.message
152
+ }))
153
+ },
154
+ suggestions: config.suggestionsEnabled ? getSuggestions(error) : [],
155
+ location: userFrame ? {
156
+ file: userFrame.originalFile || userFrame.file,
157
+ line: userFrame.originalLineNumber || userFrame.lineNumber,
158
+ column: userFrame.originalColumn || userFrame.column,
159
+ isMapped: !!userFrame.originalFile
160
+ } : null
161
+ }, null, 2);
162
+ }
163
+
89
164
  export function formatWarning(message: string, error?: Error): string {
90
165
  const config = getConfig();
91
166
  const actualError = error || new Error(message);
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { formatError, formatWarning } from './formatter/index.js';
1
+ export { formatError, formatWarning, formatErrorJSON } from './formatter/index.js';
2
2
  export { formatErrorBrowser, formatWarningBrowser } from './formatter/browser.js';
3
3
  export { setupConfig } from './config.js';
4
4
  export { registerRule } from './suggestions/index.js';
5
- export { parseStack } from './parser/index.js';
5
+ export { parseStack, getErrorChain, type ParsedStackFrame, type ErrorChain } from './parser/index.js';
6
6
  export { errorFormatter } from './middleware/index.js';
@@ -1,4 +1,8 @@
1
1
  import * as stackTraceParser from 'stacktrace-parser';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { SourceMapConsumer } from 'source-map-js';
5
+ import { getConfig } from '../config.js';
2
6
 
3
7
  export interface ParsedStackFrame {
4
8
  file: string | null;
@@ -7,8 +11,27 @@ export interface ParsedStackFrame {
7
11
  column: number | null;
8
12
  isNodeInternal: boolean;
9
13
  isNodeModule: boolean;
14
+ originalFile?: string | null;
15
+ originalLineNumber?: number | null;
16
+ originalColumn?: number | null;
10
17
  }
11
18
 
19
+ export interface ErrorChain {
20
+ error: Error;
21
+ frames: ParsedStackFrame[];
22
+ }
23
+
24
+ // Last updated: Node.js v22. Source:
25
+ // https://nodejs.org/api/module.html#modulebuiltinmodules
26
+ const NODE_BUILTINS = new Set([
27
+ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
28
+ 'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
29
+ 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'loader', 'module', 'net',
30
+ 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
31
+ 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events',
32
+ 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'
33
+ ]);
34
+
12
35
  export function parseStack(error: Error): ParsedStackFrame[] {
13
36
  if (!error.stack) return [];
14
37
 
@@ -17,16 +40,14 @@ export function parseStack(error: Error): ParsedStackFrame[] {
17
40
  return parsed.map(frame => {
18
41
  const file = frame.file || '';
19
42
 
20
- // Check if it's a built-in node module or internal
21
- // e.g. node:internal/... or just internal/... or events.js
22
43
  const isNodeInternal =
23
44
  file.startsWith('node:') ||
24
45
  file.startsWith('internal/') ||
25
- !file.includes('/') && !file.includes('\\'); // typically a core module if no path separators
46
+ NODE_BUILTINS.has(file.replace(/\.js$/, '').replace(/^node:/, ''));
26
47
 
27
48
  const isNodeModule = file.includes('node_modules');
28
49
 
29
- return {
50
+ const result: ParsedStackFrame = {
30
51
  file: frame.file,
31
52
  methodName: frame.methodName || '<unknown>',
32
53
  lineNumber: frame.lineNumber,
@@ -34,5 +55,48 @@ export function parseStack(error: Error): ParsedStackFrame[] {
34
55
  isNodeInternal: Boolean(isNodeInternal),
35
56
  isNodeModule: Boolean(isNodeModule)
36
57
  };
58
+
59
+ // Try to enrich with source maps (Node only, synchronous for now)
60
+ const config = getConfig();
61
+ if (config.sourceMapsEnabled && !isNodeInternal && !isNodeModule && frame.file && frame.lineNumber && frame.column) {
62
+ try {
63
+ const mapPath = `${frame.file}.map`;
64
+ if (fs.existsSync(mapPath)) {
65
+ const mapContent = fs.readFileSync(mapPath, 'utf-8');
66
+ const consumer = new SourceMapConsumer(JSON.parse(mapContent));
67
+ const original = consumer.originalPositionFor({
68
+ line: frame.lineNumber,
69
+ column: frame.column
70
+ });
71
+
72
+ if (original.source) {
73
+ result.originalFile = original.source;
74
+ result.originalLineNumber = original.line;
75
+ result.originalColumn = original.column;
76
+ }
77
+ }
78
+ } catch (e) {
79
+ // Silently ignore source map errors
80
+ }
81
+ }
82
+
83
+ return result;
37
84
  });
38
85
  }
86
+
87
+ export function getErrorChain(error: Error): ErrorChain[] {
88
+ const chain: ErrorChain[] = [];
89
+ let currentError: Error | undefined = error;
90
+
91
+ while (currentError) {
92
+ chain.push({
93
+ error: currentError,
94
+ frames: parseStack(currentError)
95
+ });
96
+
97
+ // Handle Error.cause (Node 16.9+, modern browsers)
98
+ currentError = (currentError as any).cause instanceof Error ? (currentError as any).cause : undefined;
99
+ }
100
+
101
+ return chain;
102
+ }
package/src/register.ts CHANGED
@@ -1,48 +1,90 @@
1
- import { formatError, formatWarning } from './formatter/index.js';
1
+ import { formatError, formatWarning, formatErrorJSON } from './formatter/index.js';
2
2
  import { formatErrorBrowser, formatWarningBrowser } from './formatter/browser.js';
3
3
  import { getConfig } from './config.js';
4
4
  import { sendWebhookNotification } from './integrations/webhook.js';
5
5
 
6
+ const isDeno = typeof (globalThis as any).Deno !== 'undefined';
7
+ const isBun = typeof (globalThis as any).Bun !== 'undefined';
8
+ const isNode = typeof process !== 'undefined' && process.release?.name === 'node';
9
+ const isEdge = typeof process !== 'undefined' && (process.env as any).NEXT_RUNTIME === 'edge';
6
10
  const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
7
11
 
8
- if (isBrowser) {
9
- // --- BROWSER REGISTRATION ---
10
- window.addEventListener('error', (event) => {
11
- // We only want to handle the error if it contains an Error object
12
- if (event.error) {
13
- console.error('%c 🛑 UNHANDLED RUNTIME ERROR:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
14
- console.log(...formatErrorBrowser(event.error));
15
- sendWebhookNotification(event.error);
12
+ if (isBrowser || isDeno) {
13
+ // --- BROWSER / DENO / WEB-STD REGISTRATION ---
14
+ const target = isBrowser ? window : globalThis;
15
+
16
+ (target as any).addEventListener('error', (event: any) => {
17
+ const error = event.error || event.message;
18
+ if (error) {
19
+ if (isBrowser) {
20
+ console.error('%c 🛑 UNHANDLED RUNTIME ERROR:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
21
+ console.log(...formatErrorBrowser(error));
22
+ } else {
23
+ const config = getConfig();
24
+ console.error('\n 🔥 UNHANDLED RUNTIME ERROR:');
25
+ if (config.useJsonMode) {
26
+ console.error(formatErrorJSON(error));
27
+ } else {
28
+ console.error(formatError(error));
29
+ }
30
+ }
31
+ sendWebhookNotification(error);
16
32
  }
17
33
  });
18
34
 
19
- window.addEventListener('unhandledrejection', (event) => {
20
- console.error('%c 🛑 UNHANDLED PROMISE REJECTION:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
35
+ (target as any).addEventListener('unhandledrejection', (event: any) => {
21
36
  const reason = event.reason;
22
37
  const error = reason instanceof Error ? reason : new Error(String(reason));
23
- console.log(...formatErrorBrowser(error));
38
+ if (isBrowser) {
39
+ console.error('%c 🛑 UNHANDLED PROMISE REJECTION:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
40
+ console.log(...formatErrorBrowser(error));
41
+ } else {
42
+ const config = getConfig();
43
+ console.error('\n 🔥 UNHANDLED PROMISE REJECTION:');
44
+ if (config.useJsonMode) {
45
+ console.error(formatErrorJSON(error));
46
+ } else {
47
+ console.error(formatError(error));
48
+ }
49
+ }
24
50
  sendWebhookNotification(error);
25
51
  });
26
52
 
27
- console.log('%c IntellError Browser listener active.', 'color: #52c41a; font-weight: bold;');
28
- } else if (typeof process !== 'undefined' && process.on) {
29
- // --- NODE.JS REGISTRATION ---
53
+ const runtimeName = isBrowser ? 'Browser' : 'Deno';
54
+ if (isBrowser) {
55
+ console.log(`%c IntellError ${runtimeName} listener active.`, 'color: #52c41a; font-weight: bold;');
56
+ } else {
57
+ console.log(`✅ IntellError ${runtimeName} listener active.`);
58
+ }
59
+ } else if (isNode || isBun || isEdge) {
60
+ // --- NODE.JS / BUN / EDGE REGISTRATION ---
30
61
  process.on('uncaughtException', (err) => {
62
+ const config = getConfig();
31
63
  console.error('\n 🔥 UNCAUGHT EXCEPTION DETECTED:');
32
- console.error(formatError(err));
64
+ if (config.useJsonMode) {
65
+ console.error(formatErrorJSON(err));
66
+ } else {
67
+ console.error(formatError(err));
68
+ }
33
69
  sendWebhookNotification(err).finally(() => {
34
- process.exit(1);
70
+ if (!isEdge) process.exit(1); // Don't exit in Edge/Serverless environments
35
71
  });
36
72
  });
37
73
 
38
74
  process.on('unhandledRejection', (reason) => {
75
+ const config = getConfig();
39
76
  console.error('\n 🔥 UNHANDLED REJECTION DETECTED:');
40
77
  const error = reason instanceof Error ? reason : new Error(String(reason));
41
- console.error(formatError(error));
78
+ if (config.useJsonMode) {
79
+ console.error(formatErrorJSON(error));
80
+ } else {
81
+ console.error(formatError(error));
82
+ }
42
83
  sendWebhookNotification(error);
43
84
  });
44
85
 
45
- console.log(' IntellError Node.js listener active.');
86
+ const runtimeName = isBun ? 'Bun' : (isEdge ? 'Edge' : 'Node.js');
87
+ console.log(`✅ IntellError ${runtimeName} listener active.`);
46
88
  }
47
89
 
48
90
  // --- OPTIONAL WARNING INTERCEPTION ---
@@ -1,12 +1,22 @@
1
- import { rules, SuggesionRule } from './rules.js';
1
+ import { rules, SuggestionRule } from './rules.js';
2
+ import { parseStack, ParsedStackFrame } from '../parser/index.js';
2
3
 
3
- export function getSuggestions(error: Error): SuggesionRule[] {
4
- return rules.filter(rule => rule.match(error));
4
+ /**
5
+ * Gets intelligent suggestions for a given error.
6
+ * Now supports context-aware matching and confidence-based sorting.
7
+ */
8
+ export function getSuggestions(error: Error): SuggestionRule[] {
9
+ const stack = parseStack(error);
10
+
11
+ return rules
12
+ .filter(rule => rule.match(error, stack))
13
+ // Sort by confidence descending
14
+ .sort((a, b) => b.confidence - a.confidence);
5
15
  }
6
16
 
7
17
  /**
8
18
  * Registers a new custom suggestion rule.
9
19
  */
10
- export function registerRule(rule: SuggesionRule) {
20
+ export function registerRule(rule: SuggestionRule) {
11
21
  rules.push(rule);
12
22
  }
@@ -1,33 +1,40 @@
1
- export interface SuggesionRule {
2
- match: (error: Error) => boolean;
1
+ import { type ParsedStackFrame } from '../parser/index.js';
2
+
3
+ export interface SuggestionRule {
4
+ match: (error: Error, stack: ParsedStackFrame[]) => boolean;
3
5
  message: string;
6
+ confidence: number; // 0-1
4
7
  fix?: string;
5
8
  description?: string;
6
9
  }
7
10
 
8
- export const rules: SuggesionRule[] = [
11
+ export const rules: SuggestionRule[] = [
9
12
  // --- TYPE ERRORS & NULL/UNDEFINED ---
10
13
  {
11
14
  match: (err) => err.message.includes("Cannot read properties of undefined (reading '"),
12
15
  message: "Accessing property on undefined object.",
16
+ confidence: 1,
13
17
  fix: "Use optional chaining like 'obj?.prop' or ensure the object is initialized.",
14
18
  description: "You're trying to access a property on a variable that is currently undefined."
15
19
  },
16
20
  {
17
21
  match: (err) => err.message.includes("Cannot read properties of null (reading '"),
18
22
  message: "Accessing property on a null object.",
23
+ confidence: 1,
19
24
  fix: "Check why the object is null before accessing its properties, or use 'obj?.prop'.",
20
25
  description: "You're trying to read a property from a variable that is null."
21
26
  },
22
27
  {
23
28
  match: (err) => err.message.includes("is not defined"),
24
29
  message: "Reference to an undeclared variable.",
30
+ confidence: 1,
25
31
  fix: "Ensure the variable is declared with 'let', 'const', or 'var' and is within its correct scope.",
26
32
  description: "You're using a variable that hasn't been defined yet."
27
33
  },
28
34
  {
29
35
  match: (err) => err.message.includes("Cannot destructure property"),
30
36
  message: "Failed to destructure object.",
37
+ confidence: 1,
31
38
  fix: "Provide a default object like 'const { x } = data || {}' to avoid destructuring null/undefined.",
32
39
  description: "You're trying to extract properties from a variable that turned out to be empty."
33
40
  },
@@ -36,18 +43,21 @@ export const rules: SuggesionRule[] = [
36
43
  {
37
44
  match: (err) => err.message.includes(".map is not a function"),
38
45
  message: "Array method '.map()' called on non-array value.",
46
+ confidence: 1,
39
47
  fix: "Verify that the variable is an array. If it's from an API, ensure the result is '[]' if empty.",
40
48
  description: "Commonly happens when an API returns an object or null instead of the expected list."
41
49
  },
42
50
  {
43
51
  match: (err) => err.message.includes(".forEach is not a function"),
44
52
  message: "Array method '.forEach()' called on non-array value.",
53
+ confidence: 1,
45
54
  fix: "Check if you're actually getting an array. Use 'Array.isArray(obj)' to verify.",
46
55
  description: "You attempted to iterate over something that isn't a list."
47
56
  },
48
57
  {
49
58
  match: (err) => err.message.includes("is not a function"),
50
59
  message: "Called a non-function value.",
60
+ confidence: 1,
51
61
  fix: "Double-check if the property you're calling is actually a function and not undefined or a string.",
52
62
  description: "You tried to execute something as a function that isn't one."
53
63
  },
@@ -56,18 +66,21 @@ export const rules: SuggesionRule[] = [
56
66
  {
57
67
  match: (err) => err instanceof SyntaxError && err.message.includes("JSON"),
58
68
  message: "Invalid JSON format.",
69
+ confidence: 1,
59
70
  fix: "Verify the source string. Use a JSON validator to find trailing commas or incorrect quotes.",
60
71
  description: "The JSON you are parsing contains syntax errors."
61
72
  },
62
73
  {
63
74
  match: (err) => err instanceof RangeError && err.message.includes("Maximum call stack size exceeded"),
64
75
  message: "Infinite recursion detected.",
76
+ confidence: 1,
65
77
  fix: "Look for functions that call themselves without a base case (e.g., missing exit condition).",
66
78
  description: "Your code likely has an infinite loop or too many nested function calls."
67
79
  },
68
80
  {
69
81
  match: (err) => err.message.includes("Invalid URL"),
70
82
  message: "Failed to construct URL.",
83
+ confidence: 1,
71
84
  fix: "Check that the input string is a valid absolute URL (e.g., starts with http:// or https://).",
72
85
  description: "The URL constructor was given a malformed or relative string."
73
86
  },
@@ -76,12 +89,14 @@ export const rules: SuggesionRule[] = [
76
89
  {
77
90
  match: (err) => err.message.includes("Failed to fetch"),
78
91
  message: "Network request failed.",
92
+ confidence: 1,
79
93
  fix: "Check your internet connection or verify if the API server is down or the URL is wrong.",
80
94
  description: "The browser could not reach the server. This is often a DNS or connectivity issue."
81
95
  },
82
96
  {
83
97
  match: (err) => err.message.includes("cors") && err.message.includes("access-control-allow-origin"),
84
98
  message: "CORS policy violation.",
99
+ confidence: 1,
85
100
  fix: "The API server needs to allow your origin. If you control the API, use the 'cors' middleware.",
86
101
  description: "The browser's security policy blocked the cross-origin request."
87
102
  },
@@ -90,24 +105,28 @@ export const rules: SuggesionRule[] = [
90
105
  {
91
106
  match: (err: any) => err.code === 'ENOENT',
92
107
  message: "File or directory not found.",
108
+ confidence: 1,
93
109
  fix: "Verify the file path. Use 'path.join(__dirname, ...)' to ensure you have an absolute path.",
94
110
  description: "Node.js could not find the file or folder you're looking for at that location."
95
111
  },
96
112
  {
97
113
  match: (err: any) => err.code === 'EADDRINUSE',
98
114
  message: "Port already in use.",
115
+ confidence: 1,
99
116
  fix: "Close the process currently using that port (e.g., kill another terminal) or change the PORT.",
100
117
  description: "Another application is already running on the port you're trying to use."
101
118
  },
102
119
  {
103
120
  match: (err: any) => err.code === 'ECONNREFUSED',
104
121
  message: "Network connection refused.",
122
+ confidence: 1,
105
123
  fix: "The target server is not listening. Ensure the API or Database is actually running.",
106
124
  description: "The connection attempt was rejected by the target machine."
107
125
  },
108
126
  {
109
127
  match: (err: any) => err.code === 'EACCES',
110
128
  message: "Permission denied.",
129
+ confidence: 1,
111
130
  fix: "Run the command with sudo/administrator privileges or check filesystem permissions.",
112
131
  description: "Your application doesn't have the internal rights to read/write to this location."
113
132
  },
@@ -116,12 +135,14 @@ export const rules: SuggesionRule[] = [
116
135
  {
117
136
  match: (err) => err.message.includes("Hooks can only be called inside of the body of a function component"),
118
137
  message: "Invalid Hook Call.",
138
+ confidence: 1,
119
139
  fix: "Ensure you aren't calling hooks inside loops, conditions, or nested functions.",
120
140
  description: "React hooks must be called at the top level of a Functional Component."
121
141
  },
122
142
  {
123
143
  match: (err) => err.message.includes("Too many re-renders"),
124
144
  message: "Infinite re-render loop.",
145
+ confidence: 1,
125
146
  fix: "You're likely updating state inside the render body. Move the update into a 'useEffect'.",
126
147
  description: "React prevents infinite loops when an update is triggered every single time a component renders."
127
148
  },
@@ -130,30 +151,35 @@ export const rules: SuggesionRule[] = [
130
151
  {
131
152
  match: (err) => err.message.includes("Cannot read properties of null (reading 'style')"),
132
153
  message: "Attempted to style a missing element.",
154
+ confidence: 1,
133
155
  fix: "The element you're trying to style wasn't found in the DOM. Verify your CSS selector (class or ID) match the HTML exactly.",
134
156
  description: "Your query (querySelector or getElementById) returned null."
135
157
  },
136
158
  {
137
159
  match: (err) => err.message.includes("is not a valid selector"),
138
160
  message: "Invalid CSS Selector.",
161
+ confidence: 1,
139
162
  fix: "Ensure your selector follows CSS rules (e.g., '.class-name', '#id-name', 'div > p'). Check for missing dots/hashes.",
140
163
  description: "The string provided to a DOM selection method is not syntactically correct."
141
164
  },
142
165
  {
143
166
  match: (err) => err.message.includes("CSSStyleDeclaration") && err.message.includes("read-only"),
144
167
  message: "Attempted to modify a read-only style property.",
168
+ confidence: 1,
145
169
  fix: "Some browser styles (like getComputedStyle results) are read-only. Use 'element.style' instead for manual changes.",
146
170
  description: "You're trying to write to a value that the browser keeps locked."
147
171
  },
148
172
  {
149
173
  match: (err) => err.message.includes("Failed to execute 'animate' on 'Element'"),
150
174
  message: "Invalid Web Animation.",
175
+ confidence: 1,
151
176
  fix: "Check your keyframes and timing options. Ensure properties like 'transform' use standard syntax.",
152
177
  description: "The Web Animations API rejected your parameters."
153
178
  },
154
179
  {
155
180
  match: (err) => err.message.includes("Failed to execute 'add' on 'DOMTokenList'") || err.message.includes("token contains HTML space characters"),
156
181
  message: "Invalid CSS Class Name.",
182
+ confidence: 1,
157
183
  fix: "CSS classes cannot contain spaces. Use 'classList.add('class1', 'class2')' with separate arguments instead.",
158
184
  description: "You tried to add an invalid token (like a sentence) into an element's className."
159
185
  },
@@ -162,18 +188,21 @@ export const rules: SuggesionRule[] = [
162
188
  {
163
189
  match: (err) => err.message.includes("Each child in a list should have a unique \"key\" prop"),
164
190
  message: "Missing React Key Prop.",
191
+ confidence: 1,
165
192
  fix: "Add a 'key' prop to the top-level element inside your .map() (e.g., <li key={item.id}>).",
166
193
  description: "React needs keys to efficiently track and update items in a list."
167
194
  },
168
195
  {
169
196
  match: (err) => err.message.includes("Changing an uncontrolled input to be controlled"),
170
197
  message: "Uncontrolled to Controlled Input switch.",
198
+ confidence: 1,
171
199
  fix: "Ensure the 'value' prop is never undefined. Initialize your state with an empty string: useState('') instead of useState().",
172
200
  description: "React expects an input to stay either controlled or uncontrolled for its entire lifecycle."
173
201
  },
174
202
  {
175
203
  match: (err) => err.message.includes("ExperimentalWarning"),
176
204
  message: "Using Experimental Node.js Feature.",
205
+ confidence: 1,
177
206
  fix: "This feature is functional but the API might change in future Node versions. High-risk for production.",
178
207
  description: "You're using a feature that is still under active development by the Node.js team."
179
208
  },
@@ -182,18 +211,21 @@ export const rules: SuggesionRule[] = [
182
211
  {
183
212
  match: (err) => err.message.includes("Cannot mix BigInt and other types"),
184
213
  message: "BigInt Type Mismatch.",
214
+ confidence: 1,
185
215
  fix: "Explicitly convert other types to BigInt using 'BigInt(value)' before performing arithmetic operations.",
186
216
  description: "JavaScript does not allow implicit coercion between BigInt and standard Number types."
187
217
  },
188
218
  {
189
219
  match: (err) => err.message.includes("is not iterable"),
190
220
  message: "Iteration Failure.",
221
+ confidence: 1,
191
222
  fix: "Check if the variable you're trying to spread or loop over (...) is null or an object instead of an Array/Map/Set.",
192
223
  description: "You attempted to use a spread operator or for...of loop on a non-iterable value."
193
224
  },
194
225
  {
195
226
  match: (err) => err instanceof SyntaxError && err.message.includes("Invalid regular expression"),
196
227
  message: "Malformed Regex.",
228
+ confidence: 1,
197
229
  fix: "Check for unclosed parentheses, invalid escape characters, or incorrect flags in your RegExp.",
198
230
  description: "The JavaScript engine could not compile your regular expression pattern."
199
231
  },
@@ -202,12 +234,14 @@ export const rules: SuggesionRule[] = [
202
234
  {
203
235
  match: (err) => err.message.includes("QuotaExceededError") || err.message.includes("exceeded the quota"),
204
236
  message: "Storage Quota Exceeded.",
237
+ confidence: 1,
205
238
  fix: "Clear some space in localStorage/IndexedDB or handle the error by clearing old cached data.",
206
239
  description: "You've reached the maximum storage limit allowed by the browser for this origin."
207
240
  },
208
241
  {
209
242
  match: (err) => err.name === 'NotAllowedError' && err.message.includes("play() failed"),
210
243
  message: "Autoplay Blocked.",
244
+ confidence: 1,
211
245
  fix: "Wait for a user interaction (click/tap) before calling .play() on audio or video elements.",
212
246
  description: "Modern browsers block media from playing automatically without a user gesture."
213
247
  },
@@ -216,12 +250,14 @@ export const rules: SuggesionRule[] = [
216
250
  {
217
251
  match: (err) => err.message.includes("Cannot set headers after they are sent to the client"),
218
252
  message: "Express Header Conflict.",
253
+ confidence: 1,
219
254
  fix: "Check for multiple 'res.send()', 'res.json()', or 'res.end()' calls in a single route handler. Use 'return res.json(...)' to stop execution early.",
220
255
  description: "You're trying to send a response to the client after a response has already been finished."
221
256
  },
222
257
  {
223
258
  match: (err: any) => err.code === 'EADDRNOTAVAIL',
224
259
  message: "Address Not Available.",
260
+ confidence: 1,
225
261
  fix: "Check your host/IP configuration. You might be trying to bind to an IP address that doesn't exist on this machine.",
226
262
  description: "The requested network address could not be assigned."
227
263
  },
@@ -230,7 +266,28 @@ export const rules: SuggesionRule[] = [
230
266
  {
231
267
  match: (err) => err.message.includes("Cannot assign to read only property 'current'"),
232
268
  message: "Immutable Ref Modification.",
269
+ confidence: 1,
233
270
  fix: "Ensure you're not trying to override a ref's .current property directly if it was provided by a parent or a third-party library.",
234
271
  description: "React protects certain ref objects from being overwritten to maintain internal consistency."
272
+ },
273
+
274
+ // --- CONTEXT-AWARE RULES (New) ---
275
+ {
276
+ match: (err, stack) =>
277
+ err.message.includes('Ref is not a valid selector') &&
278
+ stack.some(f => f.file?.includes('.tsx') || f.file?.includes('.jsx')),
279
+ message: "Ref assigned to a non-DOM element in React.",
280
+ confidence: 0.9,
281
+ fix: "Ensure you're passing 'ref' to an actual HTML tag, not a custom React component.",
282
+ description: "React components don't automatically forward refs unless they use 'forwardRef'."
283
+ },
284
+ {
285
+ match: (err, stack) =>
286
+ err.message.includes('fs') &&
287
+ stack.some(f => f.file?.includes('node_modules/vite') || f.file?.includes('node_modules/webpack')),
288
+ message: "Attempted to use 'fs' in a browser environment.",
289
+ confidence: 0.95,
290
+ fix: "The 'fs' module is only available in Node.js. Use browser-native APIs for storage or move this code to the server.",
291
+ description: "Your build tool detected an attempt to use a server-only module in the browser."
235
292
  }
236
293
  ];
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatError, formatErrorJSON } from '../src/formatter/index.js';
3
+
4
+ describe('Formatter', () => {
5
+ it('should format errors with severity levels', () => {
6
+ const error = new Error('Fatal failure');
7
+ (error as any).severity = 'FATAL';
8
+
9
+ const output = formatError(error);
10
+ expect(output).toContain('FATAL');
11
+ expect(output).toContain('Fatal failure');
12
+ });
13
+
14
+ it('should include fingerprint and timestamp in ANSI output', () => {
15
+ const error = new Error('Fingerprint test');
16
+ const output = formatError(error);
17
+
18
+ expect(output).toMatch(/ID: [a-f0-9]{8}/);
19
+ expect(output).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/);
20
+ });
21
+
22
+ it('should output valid JSON in formatErrorJSON', () => {
23
+ const error = new Error('JSON test');
24
+ const jsonStr = formatErrorJSON(error);
25
+ const json = JSON.parse(jsonStr);
26
+
27
+ expect(json.error.type).toBe('Error');
28
+ expect(json.error.message).toBe('JSON test');
29
+ expect(json.fingerprint).toBeDefined();
30
+ expect(json.timestamp).toBeDefined();
31
+ expect(json.uptime).toBeDefined();
32
+ });
33
+
34
+ it('should handle causes in JSON output', () => {
35
+ const cause = new Error('Root cause');
36
+ const error = new Error('Wrapper', { cause });
37
+
38
+ const jsonStr = formatErrorJSON(error);
39
+ const json = JSON.parse(jsonStr);
40
+
41
+ expect(json.error.causes).toHaveLength(1);
42
+ expect(json.error.causes[0].message).toBe('Root cause');
43
+ });
44
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseStack, getErrorChain } from '../src/parser/index.js';
3
+
4
+ describe('Parser', () => {
5
+ it('should identify node built-in modules correctly', () => {
6
+ const error = new Error('test');
7
+ error.stack = `Error: test
8
+ at Object.<anonymous> (fs.js:1:1)
9
+ at Module._compile (node:internal/modules/cjs/loader:1101:14)
10
+ at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
11
+ at Module.load (node:internal/modules/cjs/loader:981:32)
12
+ at Function.Module._load (node:internal/modules/cjs/loader:822:12)
13
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
14
+ at node:internal/main/run_main_module:17:47`;
15
+
16
+ const frames = parseStack(error);
17
+ expect(frames[0].isNodeInternal).toBe(true); // fs.js
18
+ expect(frames[1].isNodeInternal).toBe(true); // node:internal/...
19
+ expect(frames[6].isNodeInternal).toBe(true); // node:internal/...
20
+ });
21
+
22
+ it('should NOT identify root-level user files as node internals', () => {
23
+ const error = new Error('test');
24
+ error.stack = `Error: test
25
+ at Object.<anonymous> (index.ts:1:1)
26
+ at Module._compile (loader.js:1:1)`;
27
+
28
+ const frames = parseStack(error);
29
+ expect(frames[0].isNodeInternal).toBe(false); // index.ts is user code
30
+ expect(frames[1].isNodeInternal).toBe(true); // loader.js is internal
31
+ });
32
+
33
+ it('should traverse Error.cause correctly', () => {
34
+ const cause = new Error('Original Cause');
35
+ const error = new Error('Wrapper Error', { cause });
36
+
37
+ const chain = getErrorChain(error);
38
+ expect(chain).toHaveLength(2);
39
+ expect(chain[0].error.message).toBe('Wrapper Error');
40
+ expect(chain[1].error.message).toBe('Original Cause');
41
+ });
42
+
43
+ it('should handle anonymous frames and unknown methods', () => {
44
+ const error = new Error('test');
45
+ error.stack = `Error: test
46
+ at <anonymous>:1:1`;
47
+
48
+ const frames = parseStack(error);
49
+ expect(frames[0].methodName).toBe('<unknown>');
50
+ });
51
+ });