From c901307610e77a4a9b410b28cd08a4192f6e784a Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Wed, 10 Jun 2026 00:45:03 +0700 Subject: [PATCH 1/2] fix(util): render bigints correctly in printable error messages When bigint values appeared nested inside arrays or objects, `printable()` would output them as quoted strings (e.g. `["1n","0n","1n"]`) instead of the correct unquoted bigint notation (`[1n,0n,1n]`). Root cause: the default path in `printable()` used `JSON.stringify` on the output of `_serialize()`, which converted bigints to plain strings before `JSON.stringify` could see them, causing the strings to be double-quoted. Fix: replace `JSON.stringify(_serialize(...))` with a new `_printableStringify` helper that builds the JSON-like string directly, emitting `Xn` for bigint values instead of converting them to strings first. Closes #1477 --- ark/util/__tests__/printable.test.ts | 10 ++++ ark/util/serialize.ts | 78 +++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) 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, From 0cf8ce2c613e697713b9f2ef546155bac7aa6da5 Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Fri, 26 Jun 2026 15:26:17 +0700 Subject: [PATCH 2/2] style(util): format _printableStringify to satisfy lint --- ark/util/serialize.ts | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/ark/util/serialize.ts b/ark/util/serialize.ts index 5c288db81d..197a8a7f3c 100644 --- a/ark/util/serialize.ts +++ b/ark/util/serialize.ts @@ -140,8 +140,9 @@ const _printableStringify = ( ): 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 === "symbol") + return JSON.stringify(printableOpts.onSymbol(data)) + if (data === undefined) return JSON.stringify("undefined") if (typeof data !== "object" || data === null) return JSON.stringify(data) const o = data as object @@ -151,17 +152,24 @@ const _printableStringify = ( 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 ("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) { + if (indent) return `[\n${nextIndent}${items.join(",\n" + nextIndent)}\n${currentIndent}]` - } + return `[${items.join(",")}]` } @@ -177,11 +185,9 @@ const _printableStringify = ( nextIndent ) const serializedKey = JSON.stringify(k) - if (indent) { + if (indent) keyValues.push(`${nextIndent}${serializedKey}: ${serializedValue}`) - } else { - keyValues.push(`${serializedKey}:${serializedValue}`) - } + else keyValues.push(`${serializedKey}:${serializedValue}`) } for (const s of Object.getOwnPropertySymbols(o)) { @@ -193,17 +199,14 @@ const _printableStringify = ( nextIndent ) const serializedKey = JSON.stringify(printableOpts.onSymbol(s)) - if (indent) { + if (indent) keyValues.push(`${nextIndent}${serializedKey}: ${serializedValue}`) - } else { - keyValues.push(`${serializedKey}:${serializedValue}`) - } + else keyValues.push(`${serializedKey}:${serializedValue}`) } if (keyValues.length === 0) return "{}" - if (indent) { - return `{\n${keyValues.join(",\n")}\n${currentIndent}}` - } + if (indent) return `{\n${keyValues.join(",\n")}\n${currentIndent}}` + return `{${keyValues.join(",")}}` }