This commit is contained in:
2026-03-09 20:51:58 +03:30
parent 4102d91ced
commit a4a8f46fac
5169 changed files with 35173 additions and 437568 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "devalue",
"description": "Gets the job done when JSON.stringify can't",
"version": "5.6.3",
"version": "5.6.2",
"repository": "sveltejs/devalue",
"sideEffects": false,
"exports": {
@@ -32,6 +32,6 @@
"changeset:version": "changeset version",
"changeset:publish": "changeset publish",
"build": "dts-buddy",
"test": "uvu"
"test": "uvu test"
}
}
-1
View File
@@ -4,4 +4,3 @@ export const NAN = -3;
export const POSITIVE_INFINITY = -4;
export const NEGATIVE_INFINITY = -5;
export const NEGATIVE_ZERO = -6;
export const SPARSE = -7;
+1 -13
View File
@@ -5,7 +5,6 @@ import {
NEGATIVE_INFINITY,
NEGATIVE_ZERO,
POSITIVE_INFINITY,
SPARSE,
UNDEFINED
} from './constants.js';
@@ -202,17 +201,6 @@ export function unflatten(parsed, revivers) {
default:
throw new Error(`Unknown type ${type}`);
}
} else if (value[0] === SPARSE) {
// Sparse array encoding: [SPARSE, length, idx, val, idx, val, ...]
const len = value[1];
const array = new Array(len);
hydrated[index] = array;
for (let i = 2; i < value.length; i += 2) {
const idx = value[i];
array[idx] = hydrate(value[i + 1]);
}
} else {
const array = new Array(value.length);
hydrated[index] = array;
@@ -229,7 +217,7 @@ export function unflatten(parsed, revivers) {
const object = {};
hydrated[index] = object;
for (const key of Object.keys(value)) {
for (const key in value) {
if (key === '__proto__') {
throw new Error('Cannot parse an object with a `__proto__` property');
}
+6 -91
View File
@@ -5,8 +5,7 @@ import {
is_plain_object,
is_primitive,
stringify_key,
stringify_string,
valid_array_indices
stringify_string
} from './utils.js';
import {
HOLE,
@@ -14,7 +13,6 @@ import {
NEGATIVE_INFINITY,
NEGATIVE_ZERO,
POSITIVE_INFINITY,
SPARSE,
UNDEFINED
} from './constants.js';
import { encode64 } from './base64.js';
@@ -107,89 +105,24 @@ export function stringify(value, reducers) {
: `["RegExp",${stringify_string(source)}]`;
break;
case 'Array': {
// For dense arrays (no holes), we iterate normally.
// When we encounter the first hole, we call Object.keys
// to determine the sparseness, then decide between:
// - HOLE encoding: [-2, val, -2, ...] (default)
// - Sparse encoding: [-7, length, idx, val, ...] (for very sparse arrays)
// Only the sparse path avoids iterating every slot, which
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
let mostly_dense = false;
case 'Array':
str = '[';
for (let i = 0; i < thing.length; i += 1) {
if (i > 0) str += ',';
if (Object.hasOwn(thing, i)) {
if (i in thing) {
keys.push(`[${i}]`);
str += flatten(thing[i]);
keys.pop();
} else if (mostly_dense) {
// Use dense encoding. The heuristic guarantees the
// array is only mildly sparse, so iterating over every
// slot is fine.
str += HOLE;
} else {
// Decide between HOLE encoding and sparse encoding.
//
// HOLE encoding: each hole is serialized as the HOLE
// sentinel (-2). For example, [, "a", ,] becomes
// [-2, 0, -2]. Each hole costs 3 chars ("-2" + comma).
//
// Sparse encoding: lists only populated indices.
// For example, [, "a", ,] becomes [-7, 3, 1, 0] — the
// -7 sentinel, the array length (3), then index-value
// pairs. This avoids paying per-hole, but each element
// costs extra chars to write its index.
//
// The values are the same size either way, so the
// choice comes down to structural overhead:
//
// HOLE overhead:
// 3 chars per hole ("-2" + comma)
// = (L - P) * 3
//
// Sparse overhead:
// "-7," — 3 chars (sparse sentinel + comma)
// + length + "," — (d + 1) chars (array length + comma)
// + per element: index + "," — (d + 1) chars
// = (4 + d) + P * (d + 1)
//
// where L is the array length, P is the number of
// populated elements, and d is the number of digits
// in L (an upper bound on the digits in any index).
//
// Sparse encoding is cheaper when:
// (4 + d) + P * (d + 1) < (L - P) * 3
const populated_keys = valid_array_indices(/** @type {any[]} */ (thing));
const population = populated_keys.length;
const d = String(thing.length).length;
const hole_cost = (thing.length - population) * 3;
const sparse_cost = 4 + d + population * (d + 1);
if (hole_cost > sparse_cost) {
str = '[' + SPARSE + ',' + thing.length;
for (let j = 0; j < populated_keys.length; j++) {
const key = populated_keys[j];
keys.push(`[${key}]`);
str += ',' + key + ',' + flatten(thing[key]);
keys.pop();
}
break;
} else {
mostly_dense = true;
str += HOLE;
}
str += HOLE;
}
}
str += ']';
break;
}
case 'Set':
str = '["Set"';
@@ -284,16 +217,7 @@ export function stringify(value, reducers) {
if (Object.getPrototypeOf(thing) === null) {
str = '["null"';
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
for (const key in thing) {
keys.push(stringify_key(key));
str += `,${stringify_string(key)},${flatten(thing[key])}`;
keys.pop();
@@ -302,16 +226,7 @@ export function stringify(value, reducers) {
} else {
str = '{';
let started = false;
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
for (const key in thing) {
if (started) str += ',';
started = true;
keys.push(stringify_key(key));
+7 -90
View File
@@ -6,8 +6,7 @@ import {
is_plain_object,
is_primitive,
stringify_key,
stringify_string,
valid_array_indices
stringify_string
} from './utils.js';
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
@@ -132,16 +131,7 @@ export function uneval(value, replacer) {
);
}
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
for (const key in thing) {
keys.push(stringify_key(key));
walk(thing[key]);
keys.pop();
@@ -200,85 +190,12 @@ export function uneval(value, replacer) {
case 'URLSearchParams':
return `new URLSearchParams(${stringify_string(thing.toString())})`;
case 'Array': {
// For dense arrays (no holes), we iterate normally.
// When we encounter the first hole, we call Object.keys
// to determine the sparseness, then decide between:
// - Array literal with holes: [,"a",,] (default)
// - Object.assign: Object.assign(Array(n),{...}) (for very sparse arrays)
// Only the Object.assign path avoids iterating every slot, which
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
let has_holes = false;
let result = '[';
for (let i = 0; i < thing.length; i += 1) {
if (i > 0) result += ',';
if (Object.hasOwn(thing, i)) {
result += stringify(thing[i]);
} else if (!has_holes) {
// Decide between array literal and Object.assign.
//
// Array literal: holes are consecutive commas.
// For example, [, "a", ,] is written as [,"a",,].
// Each hole costs 1 char (a comma).
//
// Object.assign: populated indices are listed explicitly.
// For example, [, "a", ,] would be written as
// Object.assign(Array(3),{1:"a"}). This avoids paying
// per-hole, but has a large fixed overhead for the
// "Object.assign(Array(n),{...})" wrapper, and each
// element costs extra chars for its index and colon.
//
// The serialized values are the same size either way, so
// the choice comes down to the structural overhead:
//
// Array literal overhead:
// 1 char per element or hole (comma separators)
// + 2 chars for "[" and "]"
// = L + 2
//
// Object.assign overhead:
// "Object.assign(Array(" — 20 chars
// + length — d chars
// + "),{" — 3 chars
// + for each populated element:
// index + ":" + "," — (d + 2) chars
// + "})" — 2 chars
// = (25 + d) + P * (d + 2)
//
// where L is the array length, P is the number of
// populated elements, and d is the number of digits
// in L (an upper bound on the digits in any index).
//
// Object.assign is cheaper when:
// (25 + d) + P * (d + 2) < L + 2
const populated_keys = valid_array_indices(/** @type {any[]} */ (thing));
const population = populated_keys.length;
const d = String(thing.length).length;
const hole_cost = thing.length + 2;
const sparse_cost = (25 + d) + population * (d + 2);
if (hole_cost > sparse_cost) {
const entries = populated_keys
.map((k) => `${k}:${stringify(thing[k])}`)
.join(',');
return `Object.assign(Array(${thing.length}),{${entries}})`;
}
// Re-process this index as a hole in the array literal
has_holes = true;
i -= 1;
}
// else: already decided on array literal, hole is just an empty slot
// (the comma separator is all we need — no content for this position)
}
case 'Array':
const members = /** @type {any[]} */ (thing).map((v, i) =>
i in thing ? stringify(v) : ''
);
const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ',';
return result + tail + ']';
}
return `[${members.join(',')}${tail}]`;
case 'Set':
case 'Map':
-30
View File
@@ -116,33 +116,3 @@ const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
export function stringify_key(key) {
return is_identifier.test(key) ? '.' + key : '[' + JSON.stringify(key) + ']';
}
/** @param {string} s */
function is_valid_array_index(s) {
if (s.length === 0) return false;
if (s.length > 1 && s.charCodeAt(0) === 48) return false; // leading zero
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 48 || c > 57) return false;
}
// by this point we know it's a string of digits, but it has to be within the range of valid array indices
const n = +s;
if (n >= 2 ** 32 - 1) return false;
if (n < 0) return false;
return true;
}
/**
* Finds the populated indices of an array.
* @param {unknown[]} array
*/
export function valid_array_indices(array) {
const keys = Object.keys(array);
for (var i = keys.length - 1; i >= 0; i--) {
if (is_valid_array_index(keys[i])) {
break;
}
}
keys.length = i + 1;
return keys;
}