-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.ts
More file actions
173 lines (156 loc) · 5.23 KB
/
errors.ts
File metadata and controls
173 lines (156 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* @fileoverview Spawn error classification and enhancement.
*
* `isSpawnError` is a type-guard for shaping unknown errors that
* crossed an `await spawn(...)`. It checks for the
* `code` / `errno` / `syscall` properties that Node's child_process
* tags onto `ENOENT` / `EACCES` / process-exit failures.
*
* `enhanceSpawnError` rewrites the upstream `@npmcli/promise-spawn`
* "command failed" placeholder message into something the operator can
* actually act on: command + args (truncated at 100 chars), exit code
* or signal, and the first stderr line (truncated at 200 chars). The
* stack is computed lazily on first access via a per-error WeakMap so
* non-error paths don't pay the `stackWithCauses` cost.
*/
import { stackWithCauses } from '../errors/stack'
import { hasOwn } from '../objects/predicates'
import { ErrorCtor } from '../primordials/error'
import {
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptors,
} from '../primordials/object'
import { ReflectDeleteProperty } from '../primordials/reflect'
import { stackCache } from './_internal'
import type { SpawnError } from './types'
/**
* Enhances spawn error with better context.
* Converts generic "command failed" to detailed error with command, exit code, and stderr.
*
* @example
* ```typescript
* try {
* await spawn('git', ['status'])
* } catch (e) {
* throw enhanceSpawnError(e)
* }
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function enhanceSpawnError(error: unknown): unknown {
if (error === null || typeof error !== 'object') {
return error
}
if (!isSpawnError(error)) {
return error
}
const err = error as SpawnError
const { args, cmd, code, signal, stderr } = err
const stderrText =
typeof stderr === 'string' ? stderr : (stderr?.toString() ?? '')
// Build enhanced message.
let enhancedMessage = `Command failed: ${cmd}`
if (args && args.length > 0) {
const argsStr = args.join(' ')
if (argsStr.length < 100) {
enhancedMessage += ` ${argsStr}`
} else {
enhancedMessage += ` ${argsStr.slice(0, 97)}...`
}
}
// signal vs code arms exercised individually but not always paired.
// Long-line stderr fallback (>=200 chars) fires only for verbose
// child-process failures.
/* c8 ignore start */
if (signal) {
enhancedMessage += ` (terminated by ${signal})`
} else if (code !== undefined) {
enhancedMessage += ` (exit code ${code})`
}
const trimmedStderr = stderrText.trim()
if (trimmedStderr) {
const firstLine = trimmedStderr.split('\n')[0] ?? ''
if (firstLine.length < 200) {
enhancedMessage += `\n${firstLine}`
} else {
enhancedMessage += `\n${firstLine.slice(0, 197)}...`
}
}
/* c8 ignore stop */
// Check if this is a synthetic error (generic "command failed" message).
const isSynthetic = err.message === 'command failed'
if (isSynthetic) {
// Modify the error directly.
ObjectDefineProperty(err, 'message', {
__proto__: null,
value: enhancedMessage,
writable: true,
enumerable: false,
configurable: true,
} as PropertyDescriptor)
return err
}
// Create enhanced error with original error as cause.
const enhancedError = new ErrorCtor(enhancedMessage, {
cause: err,
}) as SpawnError
// Copy all spawn error properties except message and stack.
const descriptors = ObjectGetOwnPropertyDescriptors(err)
ReflectDeleteProperty(descriptors, 'message')
ReflectDeleteProperty(descriptors, 'stack')
ObjectDefineProperties(enhancedError, descriptors)
// Build stack lazily on first access using WeakMap cache.
ObjectDefineProperty(enhancedError, 'stack', {
__proto__: null,
configurable: true,
enumerable: false,
get() {
let stack = stackCache.get(enhancedError)
/* c8 ignore next - Lazy-init second-call branch on the per-error cache. */
if (stack === undefined) {
try {
stack = stackWithCauses(err)
/* c8 ignore start - stackWithCauses fallback for malformed
error chains; new ErrorCtor().stack is also ?? '' for exotic
runtimes that strip Error.stack. */
} catch {
stack = err.stack ?? new ErrorCtor().stack ?? ''
}
/* c8 ignore stop */
stackCache.set(enhancedError, stack)
}
return stack
},
} as PropertyDescriptor)
return enhancedError
}
/**
* Check if a value is a spawn error with expected error properties.
* Tests for common error properties from child process failures.
*
* @param {unknown} value - Value to check
* @returns {boolean} `true` if the value has spawn error properties
*
* @example
* try {
* await spawn('nonexistent-command')
* } catch (e) {
* if (isSpawnError(e)) {
* console.error(`Spawn failed: ${e.code}`)
* }
* }
*/
/*@__NO_SIDE_EFFECTS__*/
export function isSpawnError(value: unknown): value is SpawnError {
if (value === null || typeof value !== 'object') {
return false
}
// Check for spawn-specific error properties.
const err = value as Record<string, unknown>
return (
(hasOwn(err, 'code') && typeof err['code'] !== 'undefined') ||
(hasOwn(err, 'errno') && typeof err['errno'] !== 'undefined') ||
(hasOwn(err, 'syscall') && typeof err['syscall'] === 'string')
)
}