UNPKG

devalue

Version:

Gets the job done when JSON.stringify can't

196 lines (163 loc) 4.66 kB
import { decode64 } from './base64.js'; import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY, UNDEFINED } from './constants.js'; /** * Revive a value serialized with `devalue.stringify` * @param {string} serialized * @param {Record<string, (value: any) => any>} [revivers] */ export function parse(serialized, revivers) { return unflatten(JSON.parse(serialized), revivers); } /** * Revive a value flattened with `devalue.stringify` * @param {number | any[]} parsed * @param {Record<string, (value: any) => any>} [revivers] */ export function unflatten(parsed, revivers) { if (typeof parsed === 'number') return hydrate(parsed, true); if (!Array.isArray(parsed) || parsed.length === 0) { throw new Error('Invalid input'); } const values = /** @type {any[]} */ (parsed); const hydrated = Array(values.length); /** * @param {number} index * @returns {any} */ function hydrate(index, standalone = false) { if (index === UNDEFINED) return undefined; if (index === NAN) return NaN; if (index === POSITIVE_INFINITY) return Infinity; if (index === NEGATIVE_INFINITY) return -Infinity; if (index === NEGATIVE_ZERO) return -0; if (standalone || typeof index !== 'number') { throw new Error(`Invalid input`); } if (index in hydrated) return hydrated[index]; const value = values[index]; if (!value || typeof value !== 'object') { hydrated[index] = value; } else if (Array.isArray(value)) { if (typeof value[0] === 'string') { const type = value[0]; const reviver = revivers?.[type]; if (reviver) { return (hydrated[index] = reviver(hydrate(value[1]))); } switch (type) { case 'Date': hydrated[index] = new Date(value[1]); break; case 'Set': const set = new Set(); hydrated[index] = set; for (let i = 1; i < value.length; i += 1) { set.add(hydrate(value[i])); } break; case 'Map': const map = new Map(); hydrated[index] = map; for (let i = 1; i < value.length; i += 2) { map.set(hydrate(value[i]), hydrate(value[i + 1])); } break; case 'RegExp': hydrated[index] = new RegExp(value[1], value[2]); break; case 'Object': hydrated[index] = Object(value[1]); break; case 'BigInt': hydrated[index] = BigInt(value[1]); break; case 'null': const obj = Object.create(null); hydrated[index] = obj; for (let i = 1; i < value.length; i += 2) { obj[value[i]] = hydrate(value[i + 1]); } break; case 'Int8Array': case 'Uint8Array': case 'Uint8ClampedArray': case 'Int16Array': case 'Uint16Array': case 'Int32Array': case 'Uint32Array': case 'Float32Array': case 'Float64Array': case 'BigInt64Array': case 'BigUint64Array': { const TypedArrayConstructor = globalThis[type]; const typedArray = new TypedArrayConstructor(hydrate(value[1])); hydrated[index] = value[2] !== undefined ? typedArray.subarray(value[2], value[3]) : typedArray; break; } case 'ArrayBuffer': { const base64 = value[1]; const arraybuffer = decode64(base64); hydrated[index] = arraybuffer; break; } case 'Temporal.Duration': case 'Temporal.Instant': case 'Temporal.PlainDate': case 'Temporal.PlainTime': case 'Temporal.PlainDateTime': case 'Temporal.PlainMonthDay': case 'Temporal.PlainYearMonth': case 'Temporal.ZonedDateTime': { const temporalName = type.slice(9); // @ts-expect-error TS doesn't know about Temporal yet hydrated[index] = Temporal[temporalName].from(value[1]); break; } case 'URL': { const url = new URL(value[1]); hydrated[index] = url; break; } case 'URLSearchParams': { const url = new URLSearchParams(value[1]); hydrated[index] = url; break; } default: throw new Error(`Unknown type ${type}`); } } else { const array = new Array(value.length); hydrated[index] = array; for (let i = 0; i < value.length; i += 1) { const n = value[i]; if (n === HOLE) continue; array[i] = hydrate(n); } } } else { /** @type {Record<string, any>} */ const object = {}; hydrated[index] = object; for (const key in value) { if (key === '__proto__') { throw new Error('Cannot parse an object with a `__proto__` property'); } const n = value[key]; object[key] = hydrate(n); } } return hydrated[index]; } return hydrate(0); }