Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,43 @@ const typedArrayPrototypeGetSymbolToStringTag =
Symbol.toStringTag
).get

function isTypedArrayWithEntries (value) {
/** @param {object & {length?: unknown}} value */
function getTypedArrayType (value) {
// @ts-expect-error
return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0
const type = typedArrayPrototypeGetSymbolToStringTag.call(value)
return type !== undefined && value.length !== 0 ? type : undefined
}

/** @param {number | bigint} value */
function wrapInQuotes (value) {
return `"${value}"`
}

function stringifyTypedArray (array, separator, maximumBreadth, bigint) {
/**
* @param {ArrayLike<number | bigint>} array
* @param {string} separator
* @param {number} maximumBreadth
* @param {boolean | 'string'} bigint
* @param {((value: number | bigint) => never) | undefined} fail
* @param {string} type
*/
function stringifyTypedArray (array, separator, maximumBreadth, bigint, fail, type) {
if (array.length < maximumBreadth) {
maximumBreadth = array.length
}
const whitespace = separator === ',' ? '' : ' '
if (type[0] === 'F') {
let value = array[0]
let res = `"0":${whitespace}${Number.isFinite(value) ? value : fail ? fail(value) : 'null'}`
for (let i = 1; i < maximumBreadth; i++) {
value = array[i]
res += `${separator}"${i}":${whitespace}${Number.isFinite(value) ? value : fail ? fail(value) : 'null'}`
}
return res
}
const method = bigint === 'string' && array.length > 0 && typeof array[0] === 'bigint'
? wrapInQuotes
: String
const whitespace = separator === ',' ? '' : ' '
let res = `"0":${whitespace}${method(array[0])}`
for (let i = 1; i < maximumBreadth; i++) {
res += `${separator}"${i}":${whitespace}${method(array[i])}`
Expand Down Expand Up @@ -332,7 +352,7 @@ function configure (options) {
whitespace = ' '
}
const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
if (deterministic && !isTypedArrayWithEntries(value)) {
if (deterministic && getTypedArrayType(value) === undefined) {
keys = sort(keys, comparator)
}
stack.push(value)
Expand Down Expand Up @@ -536,8 +556,9 @@ function configure (options) {
let res = ''
let separator = ''
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
if (isTypedArrayWithEntries(value)) {
res += stringifyTypedArray(value, join, maximumBreadth, bigint)
const typedArrayType = getTypedArrayType(value)
if (typedArrayType !== undefined) {
res += stringifyTypedArray(value, join, maximumBreadth, bigint, fail, typedArrayType)
keys = keys.slice(value.length)
maximumPropertiesToStringify -= value.length
separator = join
Expand Down Expand Up @@ -645,8 +666,9 @@ function configure (options) {
}
let separator = ''
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
if (hasLength && isTypedArrayWithEntries(value)) {
res += stringifyTypedArray(value, ',', maximumBreadth, bigint)
const typedArrayType = hasLength ? getTypedArrayType(value) : undefined
if (typedArrayType !== undefined) {
res += stringifyTypedArray(value, ',', maximumBreadth, bigint, fail, typedArrayType)
keys = keys.slice(value.length)
maximumPropertiesToStringify -= value.length
separator = ','
Expand Down
71 changes: 71 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,77 @@ const { test } = require('tap')
const { stringify } = require('./index.js')
const clone = require('clone')
const { constants: { MAX_STRING_LENGTH } } = require('buffer')
const { runInNewContext } = require('vm')

for (const name of ['Float16Array', 'Float32Array', 'Float64Array']) {
const TypedArray = globalThis[/** @type {'Float16Array' | 'Float32Array' | 'Float64Array'} */ (name)]
test(`${name} serializes non-finite values`, { skip: TypedArray === undefined },
/** @param {import('tap').Test} assert */
(assert) => {
const strict = stringify.configure({ strict: true })
for (const space of [undefined, 2]) {
for (const number of [NaN, Infinity, -Infinity]) {
for (const values of [[number, 1], [1, number]]) {
const value = { metrics: new TypedArray(values) }
assert.equal(stringify(value, null, space), JSON.stringify(value, null, space))
assert.throws(() => strict(value, null, space), {
message: `Object can not safely be stringified. Received type number (${number})`
})
}
}
const finite = new TypedArray([-0, 1.5, -2])
assert.equal(strict(finite, null, space), JSON.stringify(finite, null, space))
const empty = new TypedArray(0)
assert.equal(strict(empty, null, space), JSON.stringify(empty, null, space))
const limited = stringify.configure({ strict: true, maximumBreadth: 1 })
assert.equal(limited(new TypedArray([1, NaN]), null, space),
JSON.stringify({ 0: 1, '...': '1 item not stringified' }, null, space))
}
assert.end()
})
}

test('floating typed arrays retain their intrinsic type',
/** @param {import('tap').Test} assert */
(assert) => {
class FloatSubclass extends Float32Array {}
const foreign = runInNewContext('new Float64Array([1, Infinity])')
const subclass = new FloatSubclass([1, -Infinity])
const disguised = new Float32Array([1, NaN])
Object.defineProperties(disguised, {
constructor: { value: Int32Array },
[Symbol.toStringTag]: { value: 'Int32Array' }
})
for (const value of [foreign, subclass, disguised]) {
for (const space of [undefined, 2]) {
assert.equal(stringify(value, null, space), JSON.stringify(value, null, space))
}
}
assert.end()
})

test('integer and bigint typed arrays retain their formatting',
/** @param {import('tap').Test} assert */
(assert) => {
for (const TypedArray of [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array,
Uint16Array, Int32Array, Uint32Array]) {
const value = new TypedArray([0, 1, 2])
for (const space of [undefined, 2]) {
assert.equal(stringify(value, null, space), JSON.stringify(value, null, space))
}
}
for (const TypedArray of [BigInt64Array, BigUint64Array]) {
const value = new TypedArray([0n, 1n, 2n])
for (const bigint of /** @type {const} */ ([true, 'string'])) {
const serialize = stringify.configure({ bigint })
const expected = bigint === 'string' ? { 0: '0', 1: '1', 2: '2' } : { 0: 0, 1: 1, 2: 2 }
for (const space of [undefined, 2]) {
assert.equal(serialize(value, null, space), JSON.stringify(expected, null, space))
}
}
}
assert.end()
})

/**
* @param {string} key
Expand Down
Loading