Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into fix/proto
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Sep 25, 2024
2 parents 8b90bd7 + 293d70b commit 012f36d
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# devalue changelog

## 5.1.0

- Handle typed arrays and array buffers ([#69](/~https://github.com/Rich-Harris/devalue/pull/69))
- Add `sideEffects: false` to `package.json` ([#81](/~https://github.com/Rich-Harris/devalue/pull/81))
- Better errors when keys are invalid identifiers ([#82](/~https://github.com/Rich-Harris/devalue/pull/82))

## 5.0.0

- Ignore non-enumerable symbolic keys ([#78](/~https://github.com/Rich-Harris/devalue/pull/78))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Like `JSON.stringify`, but handles
- dates
- `Map` and `Set`
- `BigInt`
- `ArrayBuffer` and Typed Arrays
- custom types via replacers, reducers and revivers

Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "devalue",
"description": "Gets the job done when JSON.stringify can't",
"version": "5.0.0",
"version": "5.1.0",
"repository": "Rich-Harris/devalue",
"sideEffects": false,
"exports": {
".": {
"types": "./types/index.d.ts",
Expand Down
110 changes: 110 additions & 0 deletions src/base64.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Base64 Encodes an arraybuffer
* @param {ArrayBuffer} arraybuffer
* @returns {string}
*/
export function encode64(arraybuffer) {
const dv = new DataView(arraybuffer);
let binaryString = "";

for (let i = 0; i < arraybuffer.byteLength; i++) {
binaryString += String.fromCharCode(dv.getUint8(i));
}

return binaryToAscii(binaryString);
}

/**
* Decodes a base64 string into an arraybuffer
* @param {string} string
* @returns {ArrayBuffer}
*/
export function decode64(string) {
const binaryString = asciiToBinary(string);
const arraybuffer = new ArrayBuffer(binaryString.length);
const dv = new DataView(arraybuffer);

for (let i = 0; i < arraybuffer.byteLength; i++) {
dv.setUint8(i, binaryString.charCodeAt(i));
}

return arraybuffer;
}

const KEY_STRING =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/**
* Substitute for atob since it's deprecated in node.
* Does not do any input validation.
*
* @see /~https://github.com/jsdom/abab/blob/master/lib/atob.js
*
* @param {string} data
* @returns {string}
*/
function asciiToBinary(data) {
if (data.length % 4 === 0) {
data = data.replace(/==?$/, "");
}

let output = "";
let buffer = 0;
let accumulatedBits = 0;

for (let i = 0; i < data.length; i++) {
buffer <<= 6;
buffer |= KEY_STRING.indexOf(data[i]);
accumulatedBits += 6;
if (accumulatedBits === 24) {
output += String.fromCharCode((buffer & 0xff0000) >> 16);
output += String.fromCharCode((buffer & 0xff00) >> 8);
output += String.fromCharCode(buffer & 0xff);
buffer = accumulatedBits = 0;
}
}
if (accumulatedBits === 12) {
buffer >>= 4;
output += String.fromCharCode(buffer);
} else if (accumulatedBits === 18) {
buffer >>= 2;
output += String.fromCharCode((buffer & 0xff00) >> 8);
output += String.fromCharCode(buffer & 0xff);
}
return output;
}

/**
* Substitute for btoa since it's deprecated in node.
* Does not do any input validation.
*
* @see /~https://github.com/jsdom/abab/blob/master/lib/btoa.js
*
* @param {string} str
* @returns {string}
*/
function binaryToAscii(str) {
let out = "";
for (let i = 0; i < str.length; i += 3) {
/** @type {[number, number, number, number]} */
const groupsOfSix = [undefined, undefined, undefined, undefined];
groupsOfSix[0] = str.charCodeAt(i) >> 2;
groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4;
if (str.length > i + 1) {
groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4;
groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2;
}
if (str.length > i + 2) {
groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6;
groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f;
}
for (let j = 0; j < groupsOfSix.length; j++) {
if (typeof groupsOfSix[j] === "undefined") {
out += "=";
} else {
out += KEY_STRING[groupsOfSix[j]];
}
}
}
return out;
}
27 changes: 27 additions & 0 deletions src/parse.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { decode64 } from './base64.js';
import {
HOLE,
NAN,
Expand Down Expand Up @@ -101,6 +102,32 @@ export function unflatten(parsed, revivers) {
}
break;

case "Int8Array":
case "Uint8Array":
case "Uint8ClampedArray":
case "Int16Array":
case "Uint16Array":
case "Int32Array":
case "Uint32Array":
case "Float32Array":
case "Float64Array":
case "BigInt64Array":
case "BigUint64Array": {
const TypedArrayConstructor = globalThis[type];
const base64 = value[1];
const arraybuffer = decode64(base64);
const typedArray = new TypedArrayConstructor(arraybuffer);
hydrated[index] = typedArray;
break;
}

case "ArrayBuffer": {
const base64 = value[1];
const arraybuffer = decode64(base64);
hydrated[index] = arraybuffer;
break;
}

default:
throw new Error(`Unknown type ${type}`);
}
Expand Down
33 changes: 31 additions & 2 deletions src/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
get_type,
is_plain_object,
is_primitive,
stringify_key,
stringify_string
} from './utils.js';
import {
Expand All @@ -14,6 +15,7 @@ import {
POSITIVE_INFINITY,
UNDEFINED
} from './constants.js';
import { encode64 } from './base64.js';

/**
* Turn a value into a JSON string that can be parsed with `devalue.parse`
Expand Down Expand Up @@ -136,6 +138,33 @@ export function stringify(value, reducers) {
str += ']';
break;

case "Int8Array":
case "Uint8Array":
case "Uint8ClampedArray":
case "Int16Array":
case "Uint16Array":
case "Int32Array":
case "Uint32Array":
case "Float32Array":
case "Float64Array":
case "BigInt64Array":
case "BigUint64Array": {
/** @type {import("./types.js").TypedArray} */
const typedArray = thing;
const base64 = encode64(typedArray.buffer);
str = '["' + type + '","' + base64 + '"]';
break;
}

case "ArrayBuffer": {
/** @type {ArrayBuffer} */
const arraybuffer = thing;
const base64 = encode64(arraybuffer);

str = `["ArrayBuffer","${base64}"]`;
break;
}

default:
if (!is_plain_object(thing)) {
throw new DevalueError(
Expand All @@ -154,7 +183,7 @@ export function stringify(value, reducers) {
if (Object.getPrototypeOf(thing) === null) {
str = '["null"';
for (const key in thing) {
keys.push(`.${key}`);
keys.push(stringify_key(key));
str += `,${stringify_string(key)},${flatten(thing[key])}`;
keys.pop();
}
Expand All @@ -165,7 +194,7 @@ export function stringify(value, reducers) {
for (const key in thing) {
if (started) str += ',';
started = true;
keys.push(`.${key}`);
keys.push(stringify_key(key));
str += `${stringify_string(key)}:${flatten(thing[key])}`;
keys.pop();
}
Expand Down
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
40 changes: 39 additions & 1 deletion src/uneval.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
get_type,
is_plain_object,
is_primitive,
stringify_key,
stringify_string
} from './utils.js';

Expand Down Expand Up @@ -81,6 +82,22 @@ export function uneval(value, replacer) {
keys.pop();
}
break;

case "Int8Array":
case "Uint8Array":
case "Uint8ClampedArray":
case "Int16Array":
case "Uint16Array":
case "Int32Array":
case "Uint32Array":
case "Float32Array":
case "Float64Array":
case "BigInt64Array":
case "BigUint64Array":
return;

case "ArrayBuffer":
return;

default:
if (!is_plain_object(thing)) {
Expand All @@ -98,7 +115,7 @@ export function uneval(value, replacer) {
}

for (const key in thing) {
keys.push(`.${key}`);
keys.push(stringify_key(key));
walk(thing[key]);
keys.pop();
}
Expand Down Expand Up @@ -160,6 +177,27 @@ export function uneval(value, replacer) {
case 'Set':
case 'Map':
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;

case "Int8Array":
case "Uint8Array":
case "Uint8ClampedArray":
case "Int16Array":
case "Uint16Array":
case "Int32Array":
case "Uint32Array":
case "Float32Array":
case "Float64Array":
case "BigInt64Array":
case "BigUint64Array": {
/** @type {import("./types.js").TypedArray} */
const typedArray = thing;
return `new ${type}([${typedArray.toString()}])`;
}

case "ArrayBuffer": {
const ui8 = new Uint8Array(thing);
return `new Uint8Array([${ui8.toString()}]).buffer`;
}

default:
const obj = `{${Object.keys(thing)
Expand Down
7 changes: 7 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ export function enumerable_symbols(object) {
(symbol) => Object.getOwnPropertyDescriptor(object, symbol).enumerable
);
}

const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;

/** @param {string} key */
export function stringify_key(key) {
return is_identifier.test(key) ? '.' + key : '[' + JSON.stringify(key) + ']';
}
16 changes: 14 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ const fixtures = {
value: BigInt('1'),
js: '1n',
json: '[["BigInt","1"]]'
},
{
name: 'Uint8Array',
value: new Uint8Array([1, 2, 3]),
js: 'new Uint8Array([1,2,3])',
json: '[["Uint8Array","AQID"]]'
},
{
name: "ArrayBuffer",
value: new Uint8Array([1, 2, 3]).buffer,
js: 'new Uint8Array([1,2,3]).buffer',
json: '[["ArrayBuffer","AQID"]]'
}
],

Expand Down Expand Up @@ -584,13 +596,13 @@ for (const fn of [uneval, stringify]) {
class Whatever {}
fn({
foo: {
map: new Map([['key', new Whatever()]])
['string-key']: new Map([['key', new Whatever()]])
}
});
} catch (e) {
assert.equal(e.name, 'DevalueError');
assert.equal(e.message, 'Cannot stringify arbitrary non-POJOs');
assert.equal(e.path, '.foo.map.get("key")');
assert.equal(e.path, '.foo["string-key"].get("key")');
}
});

Expand Down

0 comments on commit 012f36d

Please sign in to comment.