Skip to content

Commit 28f9331

Browse files
committed
combine serializeValue and makeSerializable into stringifyValue
1 parent 1b52f00 commit 28f9331

File tree

1 file changed

+62
-75
lines changed

1 file changed

+62
-75
lines changed

packages/utils/src/normalize.ts

Lines changed: 62 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { convertToPlainObject } from './object';
44
import { getFunctionName } from './stacktrace';
55

66
type UnknownMaybeWithToJson = unknown & { toJSON?: () => string };
7+
type Prototype = { constructor: (...args: unknown[]) => unknown };
78

89
/**
910
* Recursively normalizes the given object.
@@ -70,7 +71,7 @@ export function walk(
7071

7172
// If we reach the maximum depth, serialize whatever is left
7273
if (depth === 0) {
73-
return serializeValue(value);
74+
return stringifyValue(key, value);
7475
}
7576

7677
// If value implements `toJSON` method, call it and return early
@@ -81,7 +82,7 @@ export function walk(
8182
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
8283
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
8384
// all along), we're done.
84-
const serializable = makeSerializable(value, key);
85+
const serializable = stringifyValue(key, value);
8586
if (isPrimitive(serializable)) {
8687
return serializable;
8788
}
@@ -126,95 +127,81 @@ export function walk(
126127
}
127128

128129
/**
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.
131131
*
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.
161134
*
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
163137
*/
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+
}
168148

169-
if (key === 'domainEmitter') {
170-
return '[DomainEmitter]';
171-
}
149+
if (key === 'domainEmitter') {
150+
return '[DomainEmitter]';
151+
}
172152

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.
176155

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+
}
179159

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+
}
184164

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+
}
189169

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+
}
194174

195-
if (typeof value === 'number' && value !== value) {
196-
return '[NaN]';
197-
}
175+
if (typeof value === 'number' && value !== value) {
176+
return '[NaN]';
177+
}
198178

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+
}
202183

203-
if (typeof value === 'function') {
204-
return `[Function: ${getFunctionName(value)}]`;
205-
}
184+
if (typeof value === 'function') {
185+
return `[Function: ${getFunctionName(value)}]`;
186+
}
206187

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+
}
208191

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+
}
212196

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})`;
215204
}
216-
217-
return value;
218205
}
219206

220207
/** Calculates bytes size of input string */

0 commit comments

Comments
 (0)