diff --git a/ark/util/__tests__/printable.test.ts b/ark/util/__tests__/printable.test.ts index 95cb12d1c8..cab687d138 100644 --- a/ark/util/__tests__/printable.test.ts +++ b/ark/util/__tests__/printable.test.ts @@ -23,6 +23,16 @@ contextualize(() => { attest(printable(data)).snap('[1,"foo"]') }) + it("bigint in array", () => { + const data = [1n, 0n, 1n] + attest(printable(data)).snap("[1n,0n,1n]") + }) + + it("bigint in object", () => { + const data = { a: 1n, b: 2n } + attest(printable(data)).snap('{"a":1n,"b":2n}') + }) + it("nested", () => { const data = { a: [1, { b: "foo" }] } attest(printable(data)).snap('{"a":[1,{"b":"foo"}]}') diff --git a/ark/util/serialize.ts b/ark/util/serialize.ts index db6f8bd79b..5c288db81d 100644 --- a/ark/util/serialize.ts +++ b/ark/util/serialize.ts @@ -69,7 +69,7 @@ export const printable = (data: unknown, opts?: PrintableOptions): string => { ctorName === "Object" || ctorName === "Array" ? opts?.quoteKeys === false ? stringifyUnquoted(o, opts?.indent ?? 0, "") - : JSON.stringify(_serialize(o, printableOpts, []), null, opts?.indent) + : _printableStringify(o, printableOpts, [], opts?.indent ?? 0, "") : stringifyUnquoted(o, opts?.indent ?? 0, "") ) case "symbol": @@ -131,6 +131,82 @@ const printableOpts = { onFunction: v => `Function(${register(v)})` } satisfies SerializationOptions +const _printableStringify = ( + data: unknown, + opts: SerializationOptions, + seen: unknown[], + indent: number, + currentIndent: string +): string => { + if (typeof data === "function") return printableOpts.onFunction(data) + if (typeof data === "bigint") return `${data}n` + if (typeof data === "symbol") return JSON.stringify(printableOpts.onSymbol(data)) + if (typeof data === "undefined") return JSON.stringify("undefined") + if (typeof data !== "object" || data === null) return JSON.stringify(data) + + const o = data as object + + if (seen.includes(o)) return JSON.stringify("(cycle)") + + const nextSeen = [...seen, o] + const nextIndent = currentIndent + " ".repeat(indent) + + if ("toJSON" in o && typeof (o as any).toJSON === "function") + return _printableStringify((o as any).toJSON(), opts, nextSeen, indent, nextIndent) + + if (Array.isArray(o)) { + if (o.length === 0) return "[]" + const items = o.map(item => + _printableStringify(item, opts, nextSeen, indent, nextIndent) + ) + if (indent) { + return `[\n${nextIndent}${items.join(",\n" + nextIndent)}\n${currentIndent}]` + } + return `[${items.join(",")}]` + } + + if (o instanceof Date) return JSON.stringify(o.toDateString()) + + const keyValues: string[] = [] + for (const k in o) { + const serializedValue = _printableStringify( + (o as any)[k], + opts, + nextSeen, + indent, + nextIndent + ) + const serializedKey = JSON.stringify(k) + if (indent) { + keyValues.push(`${nextIndent}${serializedKey}: ${serializedValue}`) + } else { + keyValues.push(`${serializedKey}:${serializedValue}`) + } + } + + for (const s of Object.getOwnPropertySymbols(o)) { + const serializedValue = _printableStringify( + (o as any)[s], + opts, + nextSeen, + indent, + nextIndent + ) + const serializedKey = JSON.stringify(printableOpts.onSymbol(s)) + if (indent) { + keyValues.push(`${nextIndent}${serializedKey}: ${serializedValue}`) + } else { + keyValues.push(`${serializedKey}:${serializedValue}`) + } + } + + if (keyValues.length === 0) return "{}" + if (indent) { + return `{\n${keyValues.join(",\n")}\n${currentIndent}}` + } + return `{${keyValues.join(",")}}` +} + const _serialize = ( data: unknown, opts: SerializationOptions,