ts-procedures 3.1.0 → 3.2.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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Internal ts-procedures files that should be skipped when finding user code.
3
+ * Only skip the core library files, not test files or user code.
4
+ */
5
+ const INTERNAL_FILES = [
6
+ '/index.ts',
7
+ '/index.js',
8
+ '/errors.ts',
9
+ '/errors.js',
10
+ '/stack-utils.ts',
11
+ '/stack-utils.js',
12
+ '/compute-schema.ts',
13
+ '/compute-schema.js',
14
+ '/parser.ts',
15
+ '/parser.js',
16
+ ];
17
+ /**
18
+ * Captures the stack trace at the call site and extracts the definition location.
19
+ * Finds the first stack frame outside of ts-procedures internal files.
20
+ */
21
+ export function captureDefinitionInfo() {
22
+ const err = new Error();
23
+ const stack = err.stack;
24
+ if (!stack) {
25
+ return {};
26
+ }
27
+ const lines = stack.split('\n');
28
+ // Find the first frame that's not from ts-procedures internals
29
+ // Skip the first line (Error message) and frames from this module
30
+ let userFrame;
31
+ for (let i = 1; i < lines.length; i++) {
32
+ const rawLine = lines[i];
33
+ if (!rawLine)
34
+ continue;
35
+ const line = rawLine.trim();
36
+ // Skip empty or invalid frames
37
+ if (!line.startsWith('at ')) {
38
+ continue;
39
+ }
40
+ // Skip frames from ts-procedures internal source files
41
+ const isInternalFile = INTERNAL_FILES.some(file => line.includes(file));
42
+ if (isInternalFile) {
43
+ continue;
44
+ }
45
+ // Skip frames from ts-procedures in node_modules (when used as a dependency)
46
+ if (line.includes('/node_modules/ts-procedures/') || line.includes('\\node_modules\\ts-procedures\\')) {
47
+ continue;
48
+ }
49
+ // Skip internal node frames
50
+ if (line.includes('node:') || line.startsWith('at Module.') || line.startsWith('at Object.<anonymous> (node:')) {
51
+ continue;
52
+ }
53
+ userFrame = line;
54
+ break;
55
+ }
56
+ if (!userFrame) {
57
+ return { definitionStack: stack };
58
+ }
59
+ const definedAt = parseStackFrame(userFrame);
60
+ return {
61
+ definedAt,
62
+ definitionStack: stack,
63
+ };
64
+ }
65
+ /**
66
+ * Parses a V8 stack frame line to extract file, line, and column info.
67
+ * Handles formats like:
68
+ * - "at Object.<anonymous> (/path/to/file.ts:10:5)"
69
+ * - "at functionName (/path/to/file.ts:10:5)"
70
+ * - "at /path/to/file.ts:10:5"
71
+ */
72
+ function parseStackFrame(frame) {
73
+ // Match patterns like "(path:line:column)" or just "path:line:column"
74
+ const match = frame.match(/\(([^)]+):(\d+):(\d+)\)$/) || frame.match(/at ([^:]+):(\d+):(\d+)$/);
75
+ if (match && match[1] && match[2] && match[3]) {
76
+ return {
77
+ file: match[1],
78
+ line: parseInt(match[2], 10),
79
+ column: parseInt(match[3], 10),
80
+ raw: frame,
81
+ };
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * Formats definition info for appending to error stacks.
87
+ */
88
+ export function formatDefinitionInfo(info, procedureName) {
89
+ if (!info.definedAt) {
90
+ return undefined;
91
+ }
92
+ const { file, line, column } = info.definedAt;
93
+ return `\n--- Procedure "${procedureName}" defined at ---\n at ${file}:${line}:${column}`;
94
+ }
95
+ //# sourceMappingURL=stack-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack-utils.js","sourceRoot":"","sources":["../src/stack-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { captureDefinitionInfo, formatDefinitionInfo } from './stack-utils.js';
3
+ describe('Stack Utils', () => {
4
+ describe('captureDefinitionInfo', () => {
5
+ test('returns definition info with definedAt', () => {
6
+ const info = captureDefinitionInfo();
7
+ // Should capture the call site in this test file
8
+ expect(info).toBeDefined();
9
+ expect(info.definitionStack).toBeDefined();
10
+ expect(typeof info.definitionStack).toBe('string');
11
+ });
12
+ test('definedAt contains file, line, column when available', () => {
13
+ const info = captureDefinitionInfo();
14
+ // The definedAt should be present since we're calling from user code (test file)
15
+ if (info.definedAt) {
16
+ expect(info.definedAt.file).toBeDefined();
17
+ expect(typeof info.definedAt.file).toBe('string');
18
+ expect(info.definedAt.line).toBeDefined();
19
+ expect(typeof info.definedAt.line).toBe('number');
20
+ expect(info.definedAt.line).toBeGreaterThan(0);
21
+ expect(info.definedAt.column).toBeDefined();
22
+ expect(typeof info.definedAt.column).toBe('number');
23
+ expect(info.definedAt.column).toBeGreaterThan(0);
24
+ expect(info.definedAt.raw).toBeDefined();
25
+ expect(typeof info.definedAt.raw).toBe('string');
26
+ }
27
+ });
28
+ test('definitionStack contains Error stack trace', () => {
29
+ const info = captureDefinitionInfo();
30
+ expect(info.definitionStack).toContain('Error');
31
+ expect(info.definitionStack).toContain('at ');
32
+ });
33
+ });
34
+ describe('formatDefinitionInfo', () => {
35
+ test('returns undefined when definedAt is not present', () => {
36
+ const info = {};
37
+ const result = formatDefinitionInfo(info, 'TestProcedure');
38
+ expect(result).toBeUndefined();
39
+ });
40
+ test('returns formatted string when definedAt is present', () => {
41
+ const info = {
42
+ definedAt: {
43
+ file: '/app/procedures/test.ts',
44
+ line: 42,
45
+ column: 5,
46
+ raw: 'at Object.<anonymous> (/app/procedures/test.ts:42:5)',
47
+ },
48
+ };
49
+ const result = formatDefinitionInfo(info, 'TestProcedure');
50
+ expect(result).toBeDefined();
51
+ expect(result).toContain('--- Procedure "TestProcedure" defined at ---');
52
+ expect(result).toContain('/app/procedures/test.ts:42:5');
53
+ });
54
+ test('includes procedure name in formatted output', () => {
55
+ const info = {
56
+ definedAt: {
57
+ file: '/path/to/file.ts',
58
+ line: 10,
59
+ column: 3,
60
+ raw: 'at /path/to/file.ts:10:3',
61
+ },
62
+ };
63
+ const result = formatDefinitionInfo(info, 'MyCustomProcedure');
64
+ expect(result).toContain('"MyCustomProcedure"');
65
+ });
66
+ });
67
+ describe('integration with procedure creation', () => {
68
+ test('captures location from calling code', () => {
69
+ // Helper to simulate what happens in Create()
70
+ function simulateCreate() {
71
+ return captureDefinitionInfo();
72
+ }
73
+ const info = simulateCreate();
74
+ // Should have captured the location of the simulateCreate() call
75
+ expect(info).toBeDefined();
76
+ expect(info.definitionStack).toBeDefined();
77
+ });
78
+ });
79
+ });
80
+ //# sourceMappingURL=stack-utils.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack-utils.test.js","sourceRoot":"","sources":["../src/stack-utils.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAkB,MAAM,kBAAkB,CAAA;AAE9F,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACrC,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAClD,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAA;YAEpC,iDAAiD;YACjD,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1B,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1C,MAAM,CAAC,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACpD,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;YAChE,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAA;YAEpC,iFAAiF;YACjF,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACnB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;gBACzC,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;gBACjD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;gBACzC,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;gBACjD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;gBAC9C,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAA;gBAC3C,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;gBACnD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;gBAChD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;gBACxC,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YAClD,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,4CAA4C,EAAE,GAAG,EAAE;YACtD,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAA;YAEpC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;YAC/C,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACpC,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;YAC3D,MAAM,IAAI,GAAmB,EAAE,CAAA;YAC/B,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;YAE1D,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,CAAA;QAChC,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,oDAAoD,EAAE,GAAG,EAAE;YAC9D,MAAM,IAAI,GAAmB;gBAC3B,SAAS,EAAE;oBACT,IAAI,EAAE,yBAAyB;oBAC/B,IAAI,EAAE,EAAE;oBACR,MAAM,EAAE,CAAC;oBACT,GAAG,EAAE,sDAAsD;iBAC5D;aACF,CAAA;YACD,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;YAE1D,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAA;YAC5B,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,8CAA8C,CAAC,CAAA;YACxE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAA;QAC1D,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;YACvD,MAAM,IAAI,GAAmB;gBAC3B,SAAS,EAAE;oBACT,IAAI,EAAE,kBAAkB;oBACxB,IAAI,EAAE,EAAE;oBACR,MAAM,EAAE,CAAC;oBACT,GAAG,EAAE,0BAA0B;iBAChC;aACF,CAAA;YACD,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAA;YAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;QACnD,IAAI,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC/C,8CAA8C;YAC9C,SAAS,cAAc;gBACrB,OAAO,qBAAqB,EAAE,CAAA;YAChC,CAAC;YAED,MAAM,IAAI,GAAG,cAAc,EAAE,CAAA;YAE7B,iEAAiE;YACjE,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1B,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAA;QAC5C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
5
5
  "main": "build/exports.js",
6
6
  "types": "build/exports.d.ts",
@@ -4,6 +4,7 @@ import {
4
4
  ProcedureValidationError,
5
5
  ProcedureRegistrationError,
6
6
  } from './errors.js'
7
+ import { DefinitionInfo } from './stack-utils.js'
7
8
 
8
9
  describe('Error Classes', () => {
9
10
  test('ProcedureError has correct properties', () => {
@@ -51,3 +52,112 @@ describe('Error Classes', () => {
51
52
  expect(registrationErr.procedureName).toBe('Proc3')
52
53
  })
53
54
  })
55
+
56
+ describe('Error Classes - Definition Info', () => {
57
+ const mockDefinitionInfo: DefinitionInfo = {
58
+ definedAt: {
59
+ file: '/app/procedures/user.ts',
60
+ line: 25,
61
+ column: 3,
62
+ raw: 'at Object.<anonymous> (/app/procedures/user.ts:25:3)',
63
+ },
64
+ definitionStack: 'Error\n at Object.<anonymous> (/app/procedures/user.ts:25:3)',
65
+ }
66
+
67
+ test('ProcedureError includes definedAt when provided', () => {
68
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
69
+
70
+ expect(err.definedAt).toBeDefined()
71
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
72
+ expect(err.definedAt?.line).toBe(25)
73
+ expect(err.definedAt?.column).toBe(3)
74
+ })
75
+
76
+ test('ProcedureError includes definitionStack when provided', () => {
77
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
78
+
79
+ expect(err.definitionStack).toBeDefined()
80
+ expect(err.definitionStack).toContain('/app/procedures/user.ts')
81
+ })
82
+
83
+ test('ProcedureError works without definitionInfo (backward compat)', () => {
84
+ const err = new ProcedureError('TestProc', 'Something failed', { code: 123 })
85
+
86
+ expect(err.definedAt).toBeUndefined()
87
+ expect(err.definitionStack).toBeUndefined()
88
+ expect(err.procedureName).toBe('TestProc')
89
+ expect(err.message).toBe('Something failed')
90
+ expect(err.meta).toEqual({ code: 123 })
91
+ })
92
+
93
+ test('ProcedureValidationError includes definedAt when provided', () => {
94
+ const err = new ProcedureValidationError('TestProc', 'Validation failed', [], mockDefinitionInfo)
95
+
96
+ expect(err.definedAt).toBeDefined()
97
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
98
+ expect(err.definedAt?.line).toBe(25)
99
+ })
100
+
101
+ test('ProcedureValidationError works without definitionInfo (backward compat)', () => {
102
+ const err = new ProcedureValidationError('TestProc', 'Validation failed', [])
103
+
104
+ expect(err.definedAt).toBeUndefined()
105
+ expect(err.definitionStack).toBeUndefined()
106
+ expect(err.name).toBe('ProcedureValidationError')
107
+ })
108
+
109
+ test('ProcedureRegistrationError includes definedAt when provided', () => {
110
+ const err = new ProcedureRegistrationError('TestProc', 'Registration failed', mockDefinitionInfo)
111
+
112
+ expect(err.definedAt).toBeDefined()
113
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
114
+ })
115
+
116
+ test('ProcedureRegistrationError works without definitionInfo (backward compat)', () => {
117
+ const err = new ProcedureRegistrationError('TestProc', 'Registration failed')
118
+
119
+ expect(err.definedAt).toBeUndefined()
120
+ expect(err.definitionStack).toBeUndefined()
121
+ expect(err.name).toBe('ProcedureRegistrationError')
122
+ })
123
+
124
+ test('getDefinitionLocation returns formatted location string', () => {
125
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
126
+
127
+ const location = err.getDefinitionLocation()
128
+
129
+ expect(location).toBe('/app/procedures/user.ts:25:3')
130
+ })
131
+
132
+ test('getDefinitionLocation returns undefined when no definedAt', () => {
133
+ const err = new ProcedureError('TestProc', 'Something failed')
134
+
135
+ const location = err.getDefinitionLocation()
136
+
137
+ expect(location).toBeUndefined()
138
+ })
139
+
140
+ test('enhanced stack contains definition location', () => {
141
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
142
+
143
+ expect(err.stack).toContain('--- Procedure "TestProc" defined at ---')
144
+ expect(err.stack).toContain('/app/procedures/user.ts:25:3')
145
+ })
146
+
147
+ test('stack is not modified when no definitionInfo', () => {
148
+ const err = new ProcedureError('TestProc', 'Something failed')
149
+
150
+ expect(err.stack).toBeDefined()
151
+ expect(err.stack).not.toContain('--- Procedure')
152
+ })
153
+
154
+ test('all error types enhance stack with definition location', () => {
155
+ const baseErr = new ProcedureError('Proc1', 'message', undefined, mockDefinitionInfo)
156
+ const validationErr = new ProcedureValidationError('Proc2', 'message', [], mockDefinitionInfo)
157
+ const registrationErr = new ProcedureRegistrationError('Proc3', 'message', mockDefinitionInfo)
158
+
159
+ expect(baseErr.stack).toContain('--- Procedure "Proc1" defined at ---')
160
+ expect(validationErr.stack).toContain('--- Procedure "Proc2" defined at ---')
161
+ expect(registrationErr.stack).toContain('--- Procedure "Proc3" defined at ---')
162
+ })
163
+ })
package/src/errors.ts CHANGED
@@ -1,19 +1,58 @@
1
1
  import { TSchemaValidationError } from './schema/parser.js'
2
+ import { DefinitionInfo, DefinitionLocation, formatDefinitionInfo } from './stack-utils.js'
2
3
 
3
4
  export class ProcedureError extends Error {
4
5
  cause?: unknown
6
+ readonly definedAt?: DefinitionLocation
7
+ readonly definitionStack?: string
5
8
 
6
9
  constructor(
7
10
  readonly procedureName: string,
8
11
  readonly message: string,
9
12
  readonly meta?: object,
13
+ // Used for error stack trace details
14
+ definitionInfo?: DefinitionInfo
10
15
  ) {
11
16
  super(message)
12
17
  this.name = 'ProcedureError'
13
18
 
19
+ if (definitionInfo) {
20
+ this.definedAt = definitionInfo.definedAt
21
+ this.definitionStack = definitionInfo.definitionStack
22
+ this.enhanceStack()
23
+ }
24
+
14
25
  // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
15
26
  Object.setPrototypeOf(this, ProcedureError.prototype)
16
27
  }
28
+
29
+ /**
30
+ * Returns a formatted string showing where the procedure was defined.
31
+ */
32
+ getDefinitionLocation(): string | undefined {
33
+ if (!this.definedAt) {
34
+ return undefined
35
+ }
36
+ return `${this.definedAt.file}:${this.definedAt.line}:${this.definedAt.column}`
37
+ }
38
+
39
+ /**
40
+ * Enhances the error stack with definition location information.
41
+ */
42
+ private enhanceStack(): void {
43
+ if (!this.stack || !this.definedAt) {
44
+ return
45
+ }
46
+
47
+ const definitionSection = formatDefinitionInfo(
48
+ { definedAt: this.definedAt, definitionStack: this.definitionStack },
49
+ this.procedureName
50
+ )
51
+
52
+ if (definitionSection) {
53
+ this.stack = this.stack + definitionSection
54
+ }
55
+ }
17
56
  }
18
57
 
19
58
  export class ProcedureValidationError extends ProcedureError {
@@ -21,8 +60,10 @@ export class ProcedureValidationError extends ProcedureError {
21
60
  readonly procedureName: string,
22
61
  message: string,
23
62
  readonly errors?: TSchemaValidationError[],
63
+ // Used for error stack trace details
64
+ definitionInfo?: DefinitionInfo
24
65
  ) {
25
- super(procedureName, message)
66
+ super(procedureName, message, undefined, definitionInfo)
26
67
  this.name = 'ProcedureValidationError'
27
68
 
28
69
  // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
@@ -31,8 +72,13 @@ export class ProcedureValidationError extends ProcedureError {
31
72
  }
32
73
 
33
74
  export class ProcedureRegistrationError extends ProcedureError {
34
- constructor(readonly procedureName: string, message: string) {
35
- super(procedureName, message)
75
+ constructor(
76
+ readonly procedureName: string,
77
+ message: string,
78
+ // Used for error stack trace details
79
+ definitionInfo?: DefinitionInfo
80
+ ) {
81
+ super(procedureName, message, undefined, definitionInfo)
36
82
  this.name = 'ProcedureRegistrationError'
37
83
 
38
84
  // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
package/src/exports.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './index.js'
2
2
  export * from './errors.js'
3
+ export * from './stack-utils.js'
3
4
  export * from './schema/extract-json-schema.js'
4
5
  export * from './schema/parser.js'
5
6
  export * from './schema/resolve-schema-lib.js'
package/src/index.test.ts CHANGED
@@ -447,3 +447,129 @@ describe('Procedures', () => {
447
447
  }
448
448
  })
449
449
  })
450
+
451
+ describe('Procedures - Definition Location in Errors', () => {
452
+ test('ProcedureValidationError includes definition location', async () => {
453
+ const { Create } = Procedures()
454
+
455
+ const { TestValidation } = Create(
456
+ 'TestValidation',
457
+ {
458
+ schema: {
459
+ params: v.object({ name: v.string().required() }),
460
+ },
461
+ },
462
+ async (ctx, params) => {
463
+ return params.name
464
+ },
465
+ )
466
+
467
+ try {
468
+ // @ts-expect-error - intentionally passing invalid params
469
+ await TestValidation({}, {}) // Missing required 'name' param
470
+ } catch (e: any) {
471
+ expect(e).toBeInstanceOf(ProcedureValidationError)
472
+ expect(e.definedAt).toBeDefined()
473
+ expect(e.definedAt.file).toContain('index.test.ts')
474
+ expect(e.definedAt.line).toBeGreaterThan(0)
475
+ expect(e.definedAt.column).toBeGreaterThan(0)
476
+ expect(e.stack).toContain('--- Procedure "TestValidation" defined at ---')
477
+ }
478
+ })
479
+
480
+ test('ctx.error() includes definition location', async () => {
481
+ const { Create } = Procedures()
482
+
483
+ const { TestCtxError } = Create('TestCtxError', {}, async (ctx) => {
484
+ throw ctx.error('Custom error')
485
+ })
486
+
487
+ try {
488
+ await TestCtxError({}, {})
489
+ } catch (e: any) {
490
+ expect(e).toBeInstanceOf(ProcedureError)
491
+ expect(e.definedAt).toBeDefined()
492
+ expect(e.definedAt.file).toContain('index.test.ts')
493
+ expect(e.getDefinitionLocation()).toBeDefined()
494
+ expect(e.stack).toContain('--- Procedure "TestCtxError" defined at ---')
495
+ }
496
+ })
497
+
498
+ test('wrapped errors include definition location', async () => {
499
+ const { Create } = Procedures()
500
+
501
+ const { TestWrappedError } = Create('TestWrappedError', {}, async () => {
502
+ throw new Error('Original error')
503
+ })
504
+
505
+ try {
506
+ await TestWrappedError({}, {})
507
+ } catch (e: any) {
508
+ expect(e).toBeInstanceOf(ProcedureError)
509
+ expect(e.definedAt).toBeDefined()
510
+ expect(e.definedAt.file).toContain('index.test.ts')
511
+ expect(e.message).toContain('Error in handler for TestWrappedError')
512
+ expect(e.cause).toBeInstanceOf(Error)
513
+ expect(e.cause.message).toBe('Original error')
514
+ }
515
+ })
516
+
517
+ test('getDefinitionLocation returns formatted string', async () => {
518
+ const { Create } = Procedures()
519
+
520
+ const { TestGetLocation } = Create(
521
+ 'TestGetLocation',
522
+ {
523
+ schema: {
524
+ params: v.object({ id: v.number().required() }),
525
+ },
526
+ },
527
+ async (ctx, params) => {
528
+ return params.id
529
+ },
530
+ )
531
+
532
+ try {
533
+ // @ts-expect-error - intentionally passing invalid params
534
+ await TestGetLocation({}, {}) // Missing required 'id' param
535
+ } catch (e: any) {
536
+ const location = e.getDefinitionLocation()
537
+ expect(location).toBeDefined()
538
+ expect(location).toMatch(/index\.test\.ts:\d+:\d+/)
539
+ }
540
+ })
541
+
542
+ test('error stack shows procedure definition location at the end', async () => {
543
+ const { Create } = Procedures()
544
+
545
+ const { TestStackFormat } = Create(
546
+ 'TestStackFormat',
547
+ {
548
+ schema: {
549
+ params: v.object({ value: v.string().required() }),
550
+ },
551
+ },
552
+ async (ctx, params) => {
553
+ return params.value
554
+ },
555
+ )
556
+
557
+ try {
558
+ // @ts-expect-error - intentionally passing invalid params
559
+ await TestStackFormat({}, {})
560
+ } catch (e: any) {
561
+ // Verify it's a validation error
562
+ expect(e.name).toBe('ProcedureValidationError')
563
+ expect(e).toBeInstanceOf(ProcedureValidationError)
564
+ // Stack should contain the error message and definition location
565
+ expect(e.stack).toContain('Validation error for TestStackFormat')
566
+ expect(e.stack).toContain('--- Procedure "TestStackFormat" defined at ---')
567
+ // The definition section should be at the end of the stack
568
+ const stackLines = e.stack.split('\n')
569
+ const definitionIndex = stackLines.findIndex((line: string) =>
570
+ line.includes('--- Procedure "TestStackFormat" defined at ---')
571
+ )
572
+ expect(definitionIndex).toBeGreaterThan(0)
573
+ }
574
+ })
575
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ProcedureError, ProcedureValidationError } from './errors.js'
2
2
  import { computeSchema } from './schema/compute-schema.js'
3
3
  import { Prettify, TJSONSchema, TSchemaLib } from './schema/types.js'
4
+ import { captureDefinitionInfo } from './stack-utils.js'
4
5
 
5
6
  export type TNoContextProvided = unknown
6
7
 
@@ -68,16 +69,19 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
68
69
  params: TSchemaLib<TParams>
69
70
  ) => Promise<TSchemaLib<TReturnType>>
70
71
  ) {
72
+ // Capture definition location as first action
73
+ const definitionInfo = captureDefinitionInfo()
74
+
71
75
  // BEFORE computeSchema - fail fast on duplicate
72
76
  if (procedures.has(name)) {
73
77
  throw new Error(`Procedure with name ${name} is already registered`)
74
78
  }
75
79
 
76
- const { jsonSchema, validations } = computeSchema(name, config.schema)
80
+ const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
77
81
 
78
82
  // Create error factory once at registration time (outside handler)
79
83
  const errorFactory = (message: string, meta?: object) => {
80
- return new ProcedureError(name, message, meta)
84
+ return new ProcedureError(name, message, meta, definitionInfo)
81
85
  }
82
86
 
83
87
  const registeredProcedure = {
@@ -101,7 +105,8 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
101
105
  throw new ProcedureValidationError(
102
106
  name,
103
107
  `Validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`,
104
- errors
108
+ errors,
109
+ definitionInfo
105
110
  )
106
111
  }
107
112
  }
@@ -121,9 +126,20 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
121
126
  if (error instanceof ProcedureError) {
122
127
  throw error
123
128
  } else {
124
- const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`)
129
+ const err = new ProcedureError(
130
+ name,
131
+ `Error in handler for ${name} - ${error?.message}`,
132
+ undefined,
133
+ definitionInfo
134
+ )
125
135
  err.cause = error // Preserve original error
126
- err.stack = error.stack
136
+ // Preserve original stack but append definition info
137
+ if (error.stack && definitionInfo.definedAt) {
138
+ const { file, line, column } = definitionInfo.definedAt
139
+ err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
140
+ } else if (error.stack) {
141
+ err.stack = error.stack
142
+ }
127
143
  throw err
128
144
  }
129
145
  }
@@ -1,6 +1,7 @@
1
1
  import { schemaParser, TSchemaValidationError } from './parser.js'
2
2
  import { ProcedureRegistrationError } from '../errors.js'
3
3
  import { TJSONSchema } from './types.js'
4
+ import { DefinitionInfo } from '../stack-utils.js'
4
5
 
5
6
  /**
6
7
  * This function is used to compute the JSON schema and validation functions
@@ -8,6 +9,7 @@ import { TJSONSchema } from './types.js'
8
9
  *
9
10
  * @param name The name of the procedure
10
11
  * @param schema Procedure schema
12
+ * @param definitionInfo Optional definition info for error reporting
11
13
  */
12
14
  export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
13
15
  name: string,
@@ -15,6 +17,8 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
15
17
  params?: TParamsSchemaType
16
18
  returnType?: TReturnTypeSchemaType
17
19
  },
20
+ // Used for error stack trace details
21
+ definitionInfo?: DefinitionInfo
18
22
  ): {
19
23
  jsonSchema: {
20
24
  params?: TJSONSchema
@@ -43,6 +47,7 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
43
47
  `Error parsing schema for ${name} - ${Object.entries(errors)
44
48
  .map(([key, error]) => `${key}: ${error}`)
45
49
  .join(', ')}`,
50
+ definitionInfo
46
51
  )
47
52
  })
48
53