Skip to content

Commit 5590634

Browse files
mrkishiRich-Harris
andauthored
Improve platform types support (#142)
* fix: rename test file so uvu picks it up * chore: add simple benchmarks Based on Svelte's benchmark scaffolding. * feat: simplify TypedArray slices * feat: use native alternatives to encode/decode base64 * feat: whitelist `Float16Array` * fix: guarantee flatten return type * chore: add prettier configuration Add a `.prettierrc` file matching Svelte's. * chore: add missing (boxed) primitives tests * fix: support boxed bigints and boxed sentinel values * fix: reject `Symbol`s instead of producing invalid output * fix: correct typed array construction in `uneval`'s reference path * chore: add more repetition tests * fix: handle missing types in `uneval`'s reference path --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 57f73fc commit 5590634

4 files changed

Lines changed: 142 additions & 16 deletions

File tree

.changeset/petite-bushes-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'devalue': patch
3+
---
4+
5+
fix: get `uneval` type handling up to parity with `stringify`

src/stringify.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export function stringify(value, reducers) {
6767

6868
if (typeof thing === 'function') {
6969
throw new DevalueError(`Cannot stringify a function`, keys, thing, value);
70+
} else if (typeof thing === 'symbol') {
71+
throw new DevalueError(`Cannot stringify a Symbol primitive`, keys, thing, value);
7072
}
7173

7274
let str = '';

src/uneval.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export function uneval(value, replacer) {
136136
keys.pop();
137137
}
138138
}
139+
} else if (typeof thing === 'symbol') {
140+
throw new DevalueError(`Cannot stringify a Symbol primitive`, keys, thing, value);
139141
}
140142
}
141143

@@ -284,11 +286,11 @@ export function uneval(value, replacer) {
284286
case 'BigUint64Array': {
285287
let str = `new ${type}`;
286288

287-
if (counts.get(thing.buffer) === 1) {
289+
if (!names.has(thing.buffer)) {
288290
const array = new thing.constructor(thing.buffer);
289291
str += `([${array}])`;
290292
} else {
291-
str += `([${stringify(thing.buffer)}])`;
293+
str += `(${stringify(thing.buffer)})`;
292294
}
293295

294296
// handle subarrays
@@ -359,6 +361,7 @@ export function uneval(value, replacer) {
359361
case 'Number':
360362
case 'String':
361363
case 'Boolean':
364+
case 'BigInt':
362365
values.push(`Object(${stringify(thing.valueOf())})`);
363366
break;
364367

@@ -370,6 +373,14 @@ export function uneval(value, replacer) {
370373
values.push(`new Date(${thing.getTime()})`);
371374
break;
372375

376+
case 'URL':
377+
values.push(`new URL(${stringify_string(thing.toString())})`);
378+
break;
379+
380+
case 'URLSearchParams':
381+
values.push(`new URLSearchParams(${stringify_string(thing.toString())})`);
382+
break;
383+
373384
case 'Array':
374385
values.push(`Array(${thing.length})`);
375386
/** @type {any[]} */ (thing).forEach((v, i) => {
@@ -395,8 +406,41 @@ export function uneval(value, replacer) {
395406
);
396407
break;
397408

409+
case 'Int8Array':
410+
case 'Uint8Array':
411+
case 'Uint8ClampedArray':
412+
case 'Int16Array':
413+
case 'Uint16Array':
414+
case 'Float16Array':
415+
case 'Int32Array':
416+
case 'Uint32Array':
417+
case 'Float32Array':
418+
case 'Float64Array':
419+
case 'BigInt64Array':
420+
case 'BigUint64Array': {
421+
let str = `new ${type}`;
422+
423+
if (!names.has(thing.buffer)) {
424+
const array = new thing.constructor(thing.buffer);
425+
str += `([${array}])`;
426+
} else {
427+
str += `(${stringify(thing.buffer)})`;
428+
}
429+
430+
// handle subarrays
431+
if (thing.byteLength !== thing.buffer.byteLength) {
432+
const start = thing.byteOffset / thing.BYTES_PER_ELEMENT;
433+
const end = start + thing.length;
434+
str += `.subarray(${start},${end})`;
435+
}
436+
437+
values.push(`{}`);
438+
statements.push(`${name}=${str}`);
439+
break;
440+
}
441+
398442
case 'ArrayBuffer':
399-
values.push(`new Uint8Array([${new Uint8Array(thing).join(',')}]).buffer`);
443+
values.push(`new Uint8Array([${new Uint8Array(thing)}]).buffer`);
400444
break;
401445

402446
default:

test/index.test.js

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const fixtures = {
112112
js: 'null',
113113
json: '[null]'
114114
}
115+
// symbols are not supported; see further tests
115116
],
116117

117118
boxed_primitives: [
@@ -192,6 +193,7 @@ const fixtures = {
192193
json: '[["Object",1],["BigInt","1"]]'
193194
}
194195
// it's not possible to box undefined or null
196+
// boxed symbols are not supported; see further tests
195197
],
196198

197199
basics: [
@@ -564,7 +566,7 @@ const fixtures = {
564566

565567
repetition: [
566568
{
567-
name: 'String (repetition)',
569+
name: 'string (repetition)',
568570
value: ['a string', 'a string'],
569571
js: '["a string","a string"]',
570572
json: '[[1,1],"a string"]'
@@ -577,14 +579,60 @@ const fixtures = {
577579
json: '[[1,1],null]'
578580
},
579581

580-
((object) => {
581-
return {
582-
name: 'Object (repetition)',
583-
value: [object, object],
584-
js: '(function(a){return [a,a]}({}))',
585-
json: '[[1,1],{}]'
586-
};
587-
})({}),
582+
{
583+
name: 'number: NaN (repetition)',
584+
value: [NaN, NaN],
585+
js: '[NaN,NaN]',
586+
json: `[[${consts.NAN},${consts.NAN}]]`
587+
},
588+
589+
{
590+
name: 'Number (repetition)',
591+
value: ((number) => [number, number])(Object(42)),
592+
js: '(function(a){return [a,a]}(Object(42)))',
593+
json: '[[1,1],["Object",2],42]',
594+
validate: ([a, b]) => assert.is(a, b)
595+
},
596+
597+
{
598+
name: 'BigInt (repetition)',
599+
value: ((bigint) => [bigint, bigint])(Object(1n)),
600+
js: '(function(a){return [a,a]}(Object(1n)))',
601+
json: '[[1,1],["Object",2],["BigInt","1"]]',
602+
validate: ([a, b]) => assert.is(a, b)
603+
},
604+
605+
{
606+
name: 'Number: NaN (repetition)',
607+
value: ((nan) => [nan, nan])(Object(NaN)),
608+
js: '(function(a){return [a,a]}(Object(NaN)))',
609+
json: `[[1,1],["Object",${consts.NAN}]]`,
610+
validate: ([a, b]) => assert.is(a, b)
611+
},
612+
613+
{
614+
name: 'Object (repetition)',
615+
value: ((object) => [object, object])({}),
616+
js: '(function(a){return [a,a]}({}))',
617+
json: '[[1,1],{}]',
618+
validate: ([a, b]) => assert.is(a, b)
619+
},
620+
621+
{
622+
name: 'RegExp (repetition)',
623+
value: ((regexp) => [regexp, regexp])(/regexp/),
624+
js: '(function(a){return [a,a]}(/regexp/))',
625+
json: '[[1,1],["RegExp","regexp"]]',
626+
validate: ([a, b]) => assert.is(a, b)
627+
},
628+
629+
{
630+
name: 'Date (repetition)',
631+
value: ((date) => [date, date])(new Date(1e12)),
632+
js: '(function(a){return [a,a]}(new Date(1000000000000)))',
633+
json: '[[1,1],["Date","2001-09-09T01:46:40.000Z"]]',
634+
validate: ([a, b]) => assert.is(a, b)
635+
},
588636

589637
{
590638
name: 'Array buffer (repetition)',
@@ -598,11 +646,30 @@ const fixtures = {
598646

599647
return [uint8, uint16];
600648
})(),
601-
js: '(function(a){return [new Uint8Array([a]),new Uint16Array([a])]}(new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer))',
649+
js: '(function(a){return [new Uint8Array(a),new Uint16Array(a)]}(new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer))',
602650
json: '[[1,3],["Uint8Array",2],["ArrayBuffer","AAECAwQFBgcICQ=="],["Uint16Array",2]]',
603-
validate: ([uint8, uint16]) => {
604-
return uint8.buffer === uint16.buffer;
605-
}
651+
validate: ([uint8, uint16]) => assert.is(uint8.buffer, uint16.buffer)
652+
},
653+
654+
{
655+
name: 'TypedArray (repetition)',
656+
value: (() => {
657+
const uint8 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
658+
return [uint8, uint8];
659+
})(),
660+
js: '(function(a){a=new Uint8Array([0,1,2,3,4,5,6,7,8,9]);return [a,a]}({}))',
661+
json: '[[1,1],["Uint8Array",2],["ArrayBuffer","AAECAwQFBgcICQ=="]]'
662+
},
663+
664+
{
665+
name: 'Array Buffer and TypedArray (repetition)',
666+
value: (() => {
667+
const uint8 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
668+
const uint16 = new Uint16Array(uint8.buffer);
669+
return [uint8, uint8, uint16];
670+
})(),
671+
js: '(function(a,b){a=new Uint8Array(b);return [a,a,new Uint16Array(b)]}({},new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer))',
672+
json: '[[1,1,3],["Uint8Array",2],["ArrayBuffer","AAECAwQFBgcICQ=="],["Uint16Array",2]]'
606673
}
607674
],
608675

@@ -1020,6 +1087,14 @@ for (const fn of [uneval, stringify]) {
10201087
assert.throws(() => fn(foo));
10211088
});
10221089

1090+
uvu.test(`${fn.name} throws for Symbols`, () => {
1091+
assert.throws(() => fn(Symbol('foo')));
1092+
});
1093+
1094+
uvu.test(`${fn.name} throws for boxed Symbols`, () => {
1095+
assert.throws(() => fn(Object(Symbol('foo'))));
1096+
});
1097+
10231098
uvu.test(`${fn.name} throws for symbolic keys`, () => {
10241099
assert.throws(() => fn({ [Symbol()]: null }));
10251100
});

0 commit comments

Comments
 (0)