* Visits a node to perform normalization on it * * @param key The key corresponding to the given node * @param value The node to be visited * @param depth Optional number indicating the maximum recursion depth * @param maxProperties Optional maximum number of properties/elements included in any
( key: string, value: unknown, depth: number = +Infinity, maxProperties: number = +Infinity, memo = memoBuilder(), )
| 89 | * @param memo Optional Memo class handling decycling |
| 90 | */ |
| 91 | function visit( |
| 92 | key: string, |
| 93 | value: unknown, |
| 94 | depth: number = +Infinity, |
| 95 | maxProperties: number = +Infinity, |
| 96 | memo = memoBuilder(), |
| 97 | ): Primitive | ObjOrArray<unknown> { |
| 98 | const [memoize, unmemoize] = memo; |
| 99 | |
| 100 | // Get the simple cases out of the way first |
| 101 | if ( |
| 102 | value == null || // this matches null and undefined -> eqeq not eqeqeq |
| 103 | ['boolean', 'string'].includes(typeof value) || |
| 104 | (typeof value === 'number' && Number.isFinite(value)) |
| 105 | ) { |
| 106 | return value as Primitive; |
| 107 | } |
| 108 | |
| 109 | const stringified = stringifyValue(key, value); |
| 110 | |
| 111 | // Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`. |
| 112 | // Everything else will have already been serialized, so if we don't see that pattern, we're done. |
| 113 | if (!stringified.startsWith('[object ')) { |
| 114 | return stringified; |
| 115 | } |
| 116 | |
| 117 | // From here on, we can assert that `value` is either an object or an array. |
| 118 | |
| 119 | // Do not normalize objects that we know have already been normalized. Hints use internal symbols |
| 120 | // (see normalizationHints.ts) so user-controlled JSON cannot spoof them. |
| 121 | if (hasSkipNormalizationHint(value)) { |
| 122 | return value as ObjOrArray<unknown>; |
| 123 | } |
| 124 | |
| 125 | // Override remaining depth from this node (e.g. Redux / Pinia state). Set via setNormalizationDepthOverrideHint. |
| 126 | const overrideDepth = getNormalizationDepthOverrideHint(value); |
| 127 | const remainingDepth = overrideDepth !== undefined ? overrideDepth : depth; |
| 128 | |
| 129 | // We're also done if we've reached the max depth |
| 130 | if (remainingDepth === 0) { |
| 131 | // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. |
| 132 | return stringified.replace('object ', ''); |
| 133 | } |
| 134 | |
| 135 | // If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now. |
| 136 | if (memoize(value)) { |
| 137 | return '[Circular ~]'; |
| 138 | } |
| 139 | |
| 140 | // If the value has a `toJSON` method, we call it to extract more information |
| 141 | const valueWithToJSON = value as unknown & { toJSON?: () => unknown }; |
| 142 | if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') { |
| 143 | try { |
| 144 | const jsonValue = valueWithToJSON.toJSON(); |
| 145 | // We need to normalize the return value of `.toJSON()` in case it has circular references |
| 146 | return visit('', jsonValue, remainingDepth - 1, maxProperties, memo); |
| 147 | } catch { |
| 148 | // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) |
no test coverage detected