@@ -4,6 +4,7 @@ import { convertToPlainObject } from './object';
4
4
import { getFunctionName } from './stacktrace' ;
5
5
6
6
type UnknownMaybeWithToJson = unknown & { toJSON ?: ( ) => string } ;
7
+ type Prototype = { constructor : ( ...args : unknown [ ] ) => unknown } ;
7
8
8
9
/**
9
10
* Recursively normalizes the given object.
@@ -70,7 +71,7 @@ export function walk(
70
71
71
72
// If we reach the maximum depth, serialize whatever is left
72
73
if ( depth === 0 ) {
73
- return serializeValue ( value ) ;
74
+ return stringifyValue ( key , value ) ;
74
75
}
75
76
76
77
// If value implements `toJSON` method, call it and return early
@@ -81,7 +82,7 @@ export function walk(
81
82
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
82
83
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
83
84
// all along), we're done.
84
- const serializable = makeSerializable ( value , key ) ;
85
+ const serializable = stringifyValue ( key , value ) ;
85
86
if ( isPrimitive ( serializable ) ) {
86
87
return serializable ;
87
88
}
@@ -126,95 +127,81 @@ export function walk(
126
127
}
127
128
128
129
/**
129
- * Transform any non-primitive, BigInt, or Symbol-type value into a string. Acts as a no-op on strings, numbers,
130
- * booleans, null, and undefined.
130
+ * Stringify the given value. Handles various known special values and types.
131
131
*
132
- * @param value The value to stringify
133
- * @returns For non-primitive, BigInt, and Symbol-type values, a string denoting the value's type, type and value, or
134
- * type and `description` property, respectively. For non-BigInt, non-Symbol primitives, returns the original value,
135
- * unchanged.
136
- */
137
- function serializeValue ( value : any ) : any {
138
- // Node.js REPL notation
139
- if ( typeof value === 'string' ) {
140
- return value ;
141
- }
142
-
143
- const type = Object . prototype . toString . call ( value ) ;
144
- if ( type === '[object Object]' ) {
145
- return '[Object]' ;
146
- }
147
- if ( type === '[object Array]' ) {
148
- return '[Array]' ;
149
- }
150
-
151
- // `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
152
- // pass-through.
153
- const serializable = makeSerializable ( value ) ;
154
- return isPrimitive ( serializable ) ? serializable : type ;
155
- }
156
-
157
- /**
158
- * makeSerializable()
159
- *
160
- * Takes unserializable input and make it serializer-friendly.
132
+ * Not meant to be used on simple primitives which already have a string representation, as it will, for example, turn
133
+ * the number 1231 into "[Object Number]", nor on `null`, as it will throw.
161
134
*
162
- * Handles globals, functions, `undefined`, `NaN`, and other non-serializable values.
135
+ * @param value The value to stringify
136
+ * @returns A stringified representation of the given value
163
137
*/
164
- function makeSerializable < T > ( value : T , key ?: any ) : T | string {
165
- if ( key === 'domain' && value && typeof value === 'object' && ( value as unknown as { _events : any } ) . _events ) {
166
- return '[Domain]' ;
167
- }
138
+ function stringifyValue (
139
+ key : unknown ,
140
+ // this type is a tiny bit of a cheat, since this function does handle NaN (which is technically a number), but for
141
+ // our internal use, it'll do
142
+ value : Exclude < unknown , string | number | boolean | null > ,
143
+ ) : string {
144
+ try {
145
+ if ( key === 'domain' && value && typeof value === 'object' && ( value as { _events : unknown } ) . _events ) {
146
+ return '[Domain]' ;
147
+ }
168
148
169
- if ( key === 'domainEmitter' ) {
170
- return '[DomainEmitter]' ;
171
- }
149
+ if ( key === 'domainEmitter' ) {
150
+ return '[DomainEmitter]' ;
151
+ }
172
152
173
- if ( typeof ( global as any ) !== 'undefined' && ( value as unknown ) === global ) {
174
- return '[Global]' ;
175
- }
153
+ // It's safe to use `global`, `window`, and `document` here in this manner, as we are asserting using `typeof` first
154
+ // which won't throw if they are not present.
176
155
177
- // It's safe to use `window` and `document` here in this manner, as we are asserting using `typeof` first
178
- // which won't throw if they are not present.
156
+ if ( typeof global !== 'undefined' && value === global ) {
157
+ return '[Global]' ;
158
+ }
179
159
180
- // eslint-disable-next-line no-restricted-globals
181
- if ( typeof ( window as any ) !== 'undefined' && ( value as unknown ) === window ) {
182
- return '[Window]' ;
183
- }
160
+ // eslint-disable-next-line no-restricted-globals
161
+ if ( typeof window !== 'undefined' && value === window ) {
162
+ return '[Window]' ;
163
+ }
184
164
185
- // eslint-disable-next-line no-restricted-globals
186
- if ( typeof ( document as any ) !== 'undefined' && ( value as unknown ) === document ) {
187
- return '[Document]' ;
188
- }
165
+ // eslint-disable-next-line no-restricted-globals
166
+ if ( typeof document !== 'undefined' && value === document ) {
167
+ return '[Document]' ;
168
+ }
189
169
190
- // React's SyntheticEvent thingy
191
- if ( isSyntheticEvent ( value ) ) {
192
- return '[SyntheticEvent]' ;
193
- }
170
+ // React's SyntheticEvent thingy
171
+ if ( isSyntheticEvent ( value ) ) {
172
+ return '[SyntheticEvent]' ;
173
+ }
194
174
195
- if ( typeof value === 'number' && value !== value ) {
196
- return '[NaN]' ;
197
- }
175
+ if ( typeof value === 'number' && value !== value ) {
176
+ return '[NaN]' ;
177
+ }
198
178
199
- if ( value === void 0 ) {
200
- return '[undefined]' ;
201
- }
179
+ // this catches `undefined` (but not `null`, which is a primitive and can be serialized on its own)
180
+ if ( value === void 0 ) {
181
+ return '[undefined]' ;
182
+ }
202
183
203
- if ( typeof value === 'function' ) {
204
- return `[Function: ${ getFunctionName ( value ) } ]` ;
205
- }
184
+ if ( typeof value === 'function' ) {
185
+ return `[Function: ${ getFunctionName ( value ) } ]` ;
186
+ }
206
187
207
- // symbols and bigints are considered primitives by TS, but aren't natively JSON-serilaizable
188
+ if ( typeof value === 'symbol' ) {
189
+ return `[${ String ( value ) } ]` ;
190
+ }
208
191
209
- if ( typeof value === 'symbol' ) {
210
- return `[${ String ( value ) } ]` ;
211
- }
192
+ // stringified BigInts are indistinguishable from regular numbers, so we need to label them to avoid confusion
193
+ if ( typeof value === 'bigint' ) {
194
+ return `[BigInt: ${ String ( value ) } ]` ;
195
+ }
212
196
213
- if ( typeof value === 'bigint' ) {
214
- return `[BigInt: ${ String ( value ) } ]` ;
197
+ // Now that we've knocked out all the special cases and the primitives, all we have left are objects. Simply casting
198
+ // them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as
199
+ // `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class),
200
+ // we can make sure that only plain objects come out that way.
201
+ return `[object ${ ( Object . getPrototypeOf ( value ) as Prototype ) . constructor . name } ]` ;
202
+ } catch ( err ) {
203
+ return `**non-serializable** (${ err } )` ;
215
204
}
216
-
217
- return value ;
218
205
}
219
206
220
207
/** Calculates bytes size of input string */
0 commit comments