UNPKG

remix-typedjson

Version:

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that `superjson` supports, but is faster and smaller.

249 lines (248 loc) 7.81 kB
let customTypeMap = new Map(); export function registerCustomType(entry) { customTypeMap.set(entry.type, entry); } function serialize(data) { if (data === null) return { json: 'null' }; if (data === undefined) return { json: undefined }; const stack = []; const keys = ['']; const meta = new Map(); const customTypeMapValues = Array.from(customTypeMap.values()); function replacer(key, value) { function unwindStack() { while (stack.length > 0) { const top = stack[stack.length - 1]; if (top.iteration < top.count) { top.iteration++; return top; } if (top.type === 'object') { keys.pop(); } stack.pop(); } } let entry = unwindStack(); if (entry) { value = entry.value[key]; } // handle dotted keys if (key.includes('.')) { key = `[${key}]`; } let metaKey = `${keys[keys.length - 1]}${key}`; const valueType = typeof value; if (valueType === 'object' && value !== null) { let count = 0; let t = 'undefined'; if (value instanceof Date) { t = 'date'; value = value.toISOString(); } else if (value instanceof Set) { value = Array.from(value); count = value.length; t = 'set'; } else if (value instanceof Map) { value = Object.fromEntries(value); count = Object.keys(value).length; t = 'map'; } else if (value instanceof Array) { t = 'object'; count = value.length; } else if (value instanceof RegExp) { t = 'regexp'; value = String(value); } else if (value instanceof Error) { t = 'error'; value = { name: value.name, message: value.message, stack: value.stack }; // push error value to stack stack.push({ type: 'object', value, count: 3, iteration: 0 }); } else { // check for custom types let customType; if (customTypeMapValues.length > 0) { customType = customTypeMapValues.find(entry => entry.is(value)); } if (customType) { t = customType.type; value = customType.serialize(value); } else { count = Object.keys(value).length; t = 'object'; } } if (t !== 'object') { meta.set(metaKey, t); } if (count !== 0) { stack.push({ type: t, value, count, iteration: 0 }); if (key && t === 'object') { keys.push(`${metaKey}.`); } return value; } } // handle non-object types if (valueType === 'bigint') { meta.set(metaKey, 'bigint'); return String(value); } if (valueType === 'number') { if (value === Number.POSITIVE_INFINITY) { meta.set(metaKey, 'infinity'); return 'Infinity'; } if (value === Number.NEGATIVE_INFINITY) { meta.set(metaKey, '-infinity'); return '-Infinity'; } if (Number.isNaN(value)) { meta.set(metaKey, 'nan'); return 'NaN'; } } if (typeof value === 'undefined') { meta.set(metaKey, 'undefined'); return null; } return value; } const json = JSON.stringify(data, replacer); return { json, meta: meta.size === 0 ? undefined : Object.fromEntries(meta.entries()), }; } function deserialize({ json, meta }) { if (typeof json === 'undefined') { return undefined; } if (!json) return null; const result = JSON.parse(json); if (meta) { applyMeta(result, meta); } return result; } export const splitKey = (key) => { // key is a dotted path // may contain escaped dots which are keys wrapped in [] // example [b.c].d => ['b.c', 'd'] const keys = []; const parts = key.split('.'); for (let i = 0; i < parts.length; i++) { if (parts[i].startsWith('[')) { let k = parts[i].substring(1); let j = i + 1; while (!parts[j].endsWith(']')) { k += `.${parts[j]}`; j++; } k += `.${parts[j].slice(0, -1)}`; keys.push(k); i = j; } else { keys.push(parts[i]); } } return keys; }; function applyMeta(data, meta) { const customTypeMapValues = Array.from(customTypeMap.values()); for (const key of Object.keys(meta)) { const keys = splitKey(key); applyConversion(data, keys, meta[key]); } return data; function applyConversion(data, keys, type, depth = 0) { const key = keys[depth]; if (depth < keys.length - 1) { applyConversion(data[key], keys, type, depth + 1); return; } const value = data[key]; switch (type) { case 'date': data[key] = new Date(value); break; case 'set': data[key] = new Set(value); break; case 'map': data[key] = new Map(Object.entries(value)); break; case 'regexp': const match = /^\/(.*)\/([dgimsuy]*)$/.exec(value); if (match) { data[key] = new RegExp(match[1], match[2]); } else { throw new Error(`Invalid regexp: ${value}`); } break; case 'bigint': data[key] = BigInt(value); break; case 'undefined': data[key] = undefined; break; case 'infinity': data[key] = Number.POSITIVE_INFINITY; break; case '-infinity': data[key] = Number.NEGATIVE_INFINITY; break; case 'nan': data[key] = NaN; break; case 'error': const err = new Error(value.message); err.name = value.name; err.stack = value.stack; data[key] = err; break; default: // custom types let customType = customTypeMap.get(type); if (customType) { data[key] = customType.deserialize(value); } } } } function stringify(data, replacer, space) { if (replacer || space) { const { json, meta } = serialize(data); const jsonObj = deserialize({ json }); return JSON.stringify({ json: jsonObj, meta, }, replacer, space); } return JSON.stringify(serialize(data)); } function parse(json) { const result = JSON.parse(json); return result ? deserialize(result) : null; } const typedjson = { serialize, stringify, deserialize, parse, applyMeta, }; export { applyMeta, deserialize, parse, serialize, stringify }; export default typedjson;