Skip to content

Commit 012f36d

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/proto
2 parents 8b90bd7 + 293d70b commit 012f36d

File tree

10 files changed

+238
-6
lines changed

10 files changed

+238
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# devalue changelog
22

3+
## 5.1.0
4+
5+
- Handle typed arrays and array buffers ([#69](https://github.com/Rich-Harris/devalue/pull/69))
6+
- Add `sideEffects: false` to `package.json` ([#81](https://github.com/Rich-Harris/devalue/pull/81))
7+
- Better errors when keys are invalid identifiers ([#82](https://github.com/Rich-Harris/devalue/pull/82))
8+
39
## 5.0.0
410

511
- Ignore non-enumerable symbolic keys ([#78](https://github.com/Rich-Harris/devalue/pull/78))

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Like `JSON.stringify`, but handles
99
- dates
1010
- `Map` and `Set`
1111
- `BigInt`
12+
- `ArrayBuffer` and Typed Arrays
1213
- custom types via replacers, reducers and revivers
1314

1415
Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"name": "devalue",
33
"description": "Gets the job done when JSON.stringify can't",
4-
"version": "5.0.0",
4+
"version": "5.1.0",
55
"repository": "Rich-Harris/devalue",
6+
"sideEffects": false,
67
"exports": {
78
".": {
89
"types": "./types/index.d.ts",

src/base64.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Base64 Encodes an arraybuffer
3+
* @param {ArrayBuffer} arraybuffer
4+
* @returns {string}
5+
*/
6+
export function encode64(arraybuffer) {
7+
const dv = new DataView(arraybuffer);
8+
let binaryString = "";
9+
10+
for (let i = 0; i < arraybuffer.byteLength; i++) {
11+
binaryString += String.fromCharCode(dv.getUint8(i));
12+
}
13+
14+
return binaryToAscii(binaryString);
15+
}
16+
17+
/**
18+
* Decodes a base64 string into an arraybuffer
19+
* @param {string} string
20+
* @returns {ArrayBuffer}
21+
*/
22+
export function decode64(string) {
23+
const binaryString = asciiToBinary(string);
24+
const arraybuffer = new ArrayBuffer(binaryString.length);
25+
const dv = new DataView(arraybuffer);
26+
27+
for (let i = 0; i < arraybuffer.byteLength; i++) {
28+
dv.setUint8(i, binaryString.charCodeAt(i));
29+
}
30+
31+
return arraybuffer;
32+
}
33+
34+
const KEY_STRING =
35+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
36+
37+
/**
38+
* Substitute for atob since it's deprecated in node.
39+
* Does not do any input validation.
40+
*
41+
* @see https://github.com/jsdom/abab/blob/master/lib/atob.js
42+
*
43+
* @param {string} data
44+
* @returns {string}
45+
*/
46+
function asciiToBinary(data) {
47+
if (data.length % 4 === 0) {
48+
data = data.replace(/==?$/, "");
49+
}
50+
51+
let output = "";
52+
let buffer = 0;
53+
let accumulatedBits = 0;
54+
55+
for (let i = 0; i < data.length; i++) {
56+
buffer <<= 6;
57+
buffer |= KEY_STRING.indexOf(data[i]);
58+
accumulatedBits += 6;
59+
if (accumulatedBits === 24) {
60+
output += String.fromCharCode((buffer & 0xff0000) >> 16);
61+
output += String.fromCharCode((buffer & 0xff00) >> 8);
62+
output += String.fromCharCode(buffer & 0xff);
63+
buffer = accumulatedBits = 0;
64+
}
65+
}
66+
if (accumulatedBits === 12) {
67+
buffer >>= 4;
68+
output += String.fromCharCode(buffer);
69+
} else if (accumulatedBits === 18) {
70+
buffer >>= 2;
71+
output += String.fromCharCode((buffer & 0xff00) >> 8);
72+
output += String.fromCharCode(buffer & 0xff);
73+
}
74+
return output;
75+
}
76+
77+
/**
78+
* Substitute for btoa since it's deprecated in node.
79+
* Does not do any input validation.
80+
*
81+
* @see https://github.com/jsdom/abab/blob/master/lib/btoa.js
82+
*
83+
* @param {string} str
84+
* @returns {string}
85+
*/
86+
function binaryToAscii(str) {
87+
let out = "";
88+
for (let i = 0; i < str.length; i += 3) {
89+
/** @type {[number, number, number, number]} */
90+
const groupsOfSix = [undefined, undefined, undefined, undefined];
91+
groupsOfSix[0] = str.charCodeAt(i) >> 2;
92+
groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4;
93+
if (str.length > i + 1) {
94+
groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4;
95+
groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2;
96+
}
97+
if (str.length > i + 2) {
98+
groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6;
99+
groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f;
100+
}
101+
for (let j = 0; j < groupsOfSix.length; j++) {
102+
if (typeof groupsOfSix[j] === "undefined") {
103+
out += "=";
104+
} else {
105+
out += KEY_STRING[groupsOfSix[j]];
106+
}
107+
}
108+
}
109+
return out;
110+
}

src/parse.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { decode64 } from './base64.js';
12
import {
23
HOLE,
34
NAN,
@@ -101,6 +102,32 @@ export function unflatten(parsed, revivers) {
101102
}
102103
break;
103104

105+
case "Int8Array":
106+
case "Uint8Array":
107+
case "Uint8ClampedArray":
108+
case "Int16Array":
109+
case "Uint16Array":
110+
case "Int32Array":
111+
case "Uint32Array":
112+
case "Float32Array":
113+
case "Float64Array":
114+
case "BigInt64Array":
115+
case "BigUint64Array": {
116+
const TypedArrayConstructor = globalThis[type];
117+
const base64 = value[1];
118+
const arraybuffer = decode64(base64);
119+
const typedArray = new TypedArrayConstructor(arraybuffer);
120+
hydrated[index] = typedArray;
121+
break;
122+
}
123+
124+
case "ArrayBuffer": {
125+
const base64 = value[1];
126+
const arraybuffer = decode64(base64);
127+
hydrated[index] = arraybuffer;
128+
break;
129+
}
130+
104131
default:
105132
throw new Error(`Unknown type ${type}`);
106133
}

src/stringify.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
get_type,
55
is_plain_object,
66
is_primitive,
7+
stringify_key,
78
stringify_string
89
} from './utils.js';
910
import {
@@ -14,6 +15,7 @@ import {
1415
POSITIVE_INFINITY,
1516
UNDEFINED
1617
} from './constants.js';
18+
import { encode64 } from './base64.js';
1719

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

141+
case "Int8Array":
142+
case "Uint8Array":
143+
case "Uint8ClampedArray":
144+
case "Int16Array":
145+
case "Uint16Array":
146+
case "Int32Array":
147+
case "Uint32Array":
148+
case "Float32Array":
149+
case "Float64Array":
150+
case "BigInt64Array":
151+
case "BigUint64Array": {
152+
/** @type {import("./types.js").TypedArray} */
153+
const typedArray = thing;
154+
const base64 = encode64(typedArray.buffer);
155+
str = '["' + type + '","' + base64 + '"]';
156+
break;
157+
}
158+
159+
case "ArrayBuffer": {
160+
/** @type {ArrayBuffer} */
161+
const arraybuffer = thing;
162+
const base64 = encode64(arraybuffer);
163+
164+
str = `["ArrayBuffer","${base64}"]`;
165+
break;
166+
}
167+
139168
default:
140169
if (!is_plain_object(thing)) {
141170
throw new DevalueError(
@@ -154,7 +183,7 @@ export function stringify(value, reducers) {
154183
if (Object.getPrototypeOf(thing) === null) {
155184
str = '["null"';
156185
for (const key in thing) {
157-
keys.push(`.${key}`);
186+
keys.push(stringify_key(key));
158187
str += `,${stringify_string(key)},${flatten(thing[key])}`;
159188
keys.pop();
160189
}
@@ -165,7 +194,7 @@ export function stringify(value, reducers) {
165194
for (const key in thing) {
166195
if (started) str += ',';
167196
started = true;
168-
keys.push(`.${key}`);
197+
keys.push(stringify_key(key));
169198
str += `${stringify_string(key)}:${flatten(thing[key])}`;
170199
keys.pop();
171200
}

src/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;

src/uneval.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
get_type,
66
is_plain_object,
77
is_primitive,
8+
stringify_key,
89
stringify_string
910
} from './utils.js';
1011

@@ -81,6 +82,22 @@ export function uneval(value, replacer) {
8182
keys.pop();
8283
}
8384
break;
85+
86+
case "Int8Array":
87+
case "Uint8Array":
88+
case "Uint8ClampedArray":
89+
case "Int16Array":
90+
case "Uint16Array":
91+
case "Int32Array":
92+
case "Uint32Array":
93+
case "Float32Array":
94+
case "Float64Array":
95+
case "BigInt64Array":
96+
case "BigUint64Array":
97+
return;
98+
99+
case "ArrayBuffer":
100+
return;
84101

85102
default:
86103
if (!is_plain_object(thing)) {
@@ -98,7 +115,7 @@ export function uneval(value, replacer) {
98115
}
99116

100117
for (const key in thing) {
101-
keys.push(`.${key}`);
118+
keys.push(stringify_key(key));
102119
walk(thing[key]);
103120
keys.pop();
104121
}
@@ -160,6 +177,27 @@ export function uneval(value, replacer) {
160177
case 'Set':
161178
case 'Map':
162179
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;
180+
181+
case "Int8Array":
182+
case "Uint8Array":
183+
case "Uint8ClampedArray":
184+
case "Int16Array":
185+
case "Uint16Array":
186+
case "Int32Array":
187+
case "Uint32Array":
188+
case "Float32Array":
189+
case "Float64Array":
190+
case "BigInt64Array":
191+
case "BigUint64Array": {
192+
/** @type {import("./types.js").TypedArray} */
193+
const typedArray = thing;
194+
return `new ${type}([${typedArray.toString()}])`;
195+
}
196+
197+
case "ArrayBuffer": {
198+
const ui8 = new Uint8Array(thing);
199+
return `new Uint8Array([${ui8.toString()}]).buffer`;
200+
}
163201

164202
default:
165203
const obj = `{${Object.keys(thing)

src/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,10 @@ export function enumerable_symbols(object) {
104104
(symbol) => Object.getOwnPropertyDescriptor(object, symbol).enumerable
105105
);
106106
}
107+
108+
const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
109+
110+
/** @param {string} key */
111+
export function stringify_key(key) {
112+
return is_identifier.test(key) ? '.' + key : '[' + JSON.stringify(key) + ']';
113+
}

test/test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ const fixtures = {
159159
value: BigInt('1'),
160160
js: '1n',
161161
json: '[["BigInt","1"]]'
162+
},
163+
{
164+
name: 'Uint8Array',
165+
value: new Uint8Array([1, 2, 3]),
166+
js: 'new Uint8Array([1,2,3])',
167+
json: '[["Uint8Array","AQID"]]'
168+
},
169+
{
170+
name: "ArrayBuffer",
171+
value: new Uint8Array([1, 2, 3]).buffer,
172+
js: 'new Uint8Array([1,2,3]).buffer',
173+
json: '[["ArrayBuffer","AQID"]]'
162174
}
163175
],
164176

@@ -584,13 +596,13 @@ for (const fn of [uneval, stringify]) {
584596
class Whatever {}
585597
fn({
586598
foo: {
587-
map: new Map([['key', new Whatever()]])
599+
['string-key']: new Map([['key', new Whatever()]])
588600
}
589601
});
590602
} catch (e) {
591603
assert.equal(e.name, 'DevalueError');
592604
assert.equal(e.message, 'Cannot stringify arbitrary non-POJOs');
593-
assert.equal(e.path, '.foo.map.get("key")');
605+
assert.equal(e.path, '.foo["string-key"].get("key")');
594606
}
595607
});
596608

0 commit comments

Comments
 (0)