Comparing version 3.0.1 to 3.1.0
# devalue changelog | ||
## 3.1.0 | ||
- Include `path` in error object if value is unserializable | ||
## 3.0.1 | ||
@@ -4,0 +8,0 @@ |
@@ -25,2 +25,14 @@ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; | ||
class DevalueError extends Error { | ||
/** | ||
* @param {string} message | ||
* @param {string[]} keys | ||
*/ | ||
constructor(message, keys) { | ||
super(message); | ||
this.name = 'DevalueError'; | ||
this.path = keys.join(''); | ||
} | ||
} | ||
/** | ||
@@ -33,6 +45,9 @@ * Turn a value into the JavaScript that creates an equivalent value | ||
/** @type {string[]} */ | ||
const keys = []; | ||
/** @param {any} thing */ | ||
function walk(thing) { | ||
if (typeof thing === 'function') { | ||
throw new Error(`Cannot stringify a function`); | ||
throw new DevalueError(`Cannot stringify a function`, keys); | ||
} | ||
@@ -60,10 +75,23 @@ | ||
case 'Array': | ||
thing.forEach(walk); | ||
/** @type {any[]} */ (thing).forEach((value, i) => { | ||
keys.push(`[${i}]`); | ||
walk(value); | ||
keys.pop(); | ||
}); | ||
break; | ||
case 'Set': | ||
case 'Map': | ||
Array.from(thing).forEach(walk); | ||
break; | ||
case 'Map': | ||
for (const [key, value] of thing) { | ||
keys.push( | ||
`.get(${is_primitive(key) ? stringify_primitive(key) : '...'})` | ||
); | ||
walk(value); | ||
keys.pop(); | ||
} | ||
break; | ||
default: | ||
@@ -78,10 +106,20 @@ const proto = Object.getPrototypeOf(thing); | ||
) { | ||
throw new Error(`Cannot stringify arbitrary non-POJOs`); | ||
throw new DevalueError( | ||
`Cannot stringify arbitrary non-POJOs`, | ||
keys | ||
); | ||
} | ||
if (Object.getOwnPropertySymbols(thing).length > 0) { | ||
throw new Error(`Cannot stringify POJOs with symbolic keys`); | ||
throw new DevalueError( | ||
`Cannot stringify POJOs with symbolic keys`, | ||
keys | ||
); | ||
} | ||
Object.keys(thing).forEach((key) => walk(thing[key])); | ||
for (const key in thing) { | ||
keys.push(`.${key}`); | ||
walk(thing[key]); | ||
keys.pop(); | ||
} | ||
} | ||
@@ -88,0 +126,0 @@ } |
import * as vm from 'vm'; | ||
import * as assert from 'uvu/assert'; | ||
import * as uvu from 'uvu'; | ||
import { devalue } from './devalue.js'; | ||
let passed = 0; | ||
let failed = 0; | ||
/** | ||
@@ -15,52 +14,14 @@ * @typedef {(name: string, input: any, expected: string) => void} TestFunction | ||
*/ | ||
function describe(name, fn) { | ||
console.group(`\n${name}`); | ||
function compare(name, fn) { | ||
const test = uvu.suite(name); | ||
fn((name, input, expected) => { | ||
const actual = devalue(input); | ||
if (actual === expected) { | ||
console.log(`✅ ${name}`); | ||
passed += 1; | ||
} else { | ||
console.log(`❌ ${name}`); | ||
console.log(` actual: ${actual}`); | ||
console.log(` expected: ${expected}`); | ||
failed += 1; | ||
} | ||
test(name, () => { | ||
const actual = devalue(input); | ||
assert.equal(actual, expected); | ||
}); | ||
}); | ||
console.groupEnd(); | ||
test.run(); | ||
} | ||
/** | ||
* | ||
* @param {string} name | ||
* @param {(...args: any[]) => void} fn | ||
*/ | ||
function throws(name, fn) { | ||
try { | ||
fn(); | ||
console.log(`❌ ${name}`); | ||
failed += 1; | ||
} catch (e) { | ||
console.log(`✅ ${name}`); | ||
passed += 1; | ||
} | ||
} | ||
/** | ||
* | ||
* @param {string} name | ||
* @param {(...args: any[]) => void} fn | ||
*/ | ||
function allows(name, fn) { | ||
try { | ||
fn(); | ||
console.log(`✅ ${name}`); | ||
passed += 1; | ||
} catch (e) { | ||
console.log(`❌ ${name} (${e.message})`); | ||
failed += 1; | ||
} | ||
} | ||
describe('basics', (t) => { | ||
compare('basics', (t) => { | ||
t('number', 42, '42'); | ||
@@ -91,3 +52,3 @@ t('negative number', -42, '-42'); | ||
describe('strings', (t) => { | ||
compare('strings', (t) => { | ||
t('newline', 'a\nb', JSON.stringify('a\nb')); | ||
@@ -105,3 +66,3 @@ t('double quotes', '"yar"', JSON.stringify('"yar"')); | ||
describe('cycles', (t) => { | ||
compare('cycles', (t) => { | ||
let map = new Map(); | ||
@@ -150,3 +111,3 @@ map.set('self', map); | ||
describe('repetition', (t) => { | ||
compare('repetition', (t) => { | ||
let str = 'a string'; | ||
@@ -160,3 +121,3 @@ t( | ||
describe('XSS', (t) => { | ||
compare('XSS', (t) => { | ||
t( | ||
@@ -179,34 +140,52 @@ 'Dangerous string', | ||
describe('misc', (t) => { | ||
compare('misc', (t) => { | ||
t('Object without prototype', Object.create(null), 'Object.create(null)'); | ||
t('cross-realm POJO', vm.runInNewContext('({})'), '{}'); | ||
}); | ||
// let arr = []; | ||
// arr.x = 42; | ||
// test('Array with named properties', arr, `TODO`); | ||
uvu.test('throws for non-POJOs', () => { | ||
class Foo {} | ||
const foo = new Foo(); | ||
assert.throws(() => devalue(foo)); | ||
}); | ||
t('cross-realm POJO', vm.runInNewContext('({})'), '{}'); | ||
uvu.test('throws for symbolic keys', () => { | ||
assert.throws(() => devalue({ [Symbol()]: null })); | ||
}); | ||
throws('throws for non-POJOs', () => { | ||
class Foo {} | ||
const foo = new Foo(); | ||
devalue(foo); | ||
}); | ||
uvu.test('does not create duplicate parameter names', () => { | ||
const foo = new Array(20000).fill(0).map((_, i) => i); | ||
const bar = foo.map((_, i) => ({ [i]: foo[i] })); | ||
const serialized = devalue([foo, ...bar]); | ||
throws('throws for symbolic keys', () => { | ||
devalue({ [Symbol()]: null }); | ||
}); | ||
eval(serialized); | ||
}); | ||
allows('does not create duplicate parameter names', () => { | ||
const foo = new Array(20000).fill(0).map((_, i) => i); | ||
const bar = foo.map((_, i) => ({ [i]: foo[i] })); | ||
const serialized = devalue([foo, ...bar]); | ||
uvu.test('populates error.keys and error.path', () => { | ||
try { | ||
devalue({ | ||
foo: { | ||
array: [function invalid() {}] | ||
} | ||
}); | ||
} catch (e) { | ||
assert.equal(e.name, 'DevalueError'); | ||
assert.equal(e.message, 'Cannot stringify a function'); | ||
assert.equal(e.path, '.foo.array[0]'); | ||
} | ||
eval(serialized); | ||
}); | ||
try { | ||
class Whatever {} | ||
devalue({ | ||
foo: { | ||
map: 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")'); | ||
} | ||
}); | ||
console.log(`\n---\n${passed} passed, ${failed} failed\n`); | ||
if (failed > 0) { | ||
process.exit(1); | ||
} | ||
uvu.test.run(); |
{ | ||
"name": "devalue", | ||
"description": "Gets the job done when JSON.stringify can't", | ||
"version": "3.0.1", | ||
"version": "3.1.0", | ||
"repository": "Rich-Harris/devalue", | ||
@@ -13,3 +13,4 @@ "exports": { | ||
"devDependencies": { | ||
"typescript": "^3.1.3" | ||
"typescript": "^3.1.3", | ||
"uvu": "^0.5.6" | ||
}, | ||
@@ -16,0 +17,0 @@ "scripts": { |
@@ -40,4 +40,21 @@ # devalue | ||
If `devalue` encounters a function or a non-POJO, it will throw an error. | ||
## Error handling | ||
If `devalue` encounters a function or a non-POJO, it will throw an error. You can find where in the input data the offending value lives by inspecting `error.path`: | ||
```js | ||
try { | ||
const map = new Map(); | ||
map.set('key', function invalid() {}); | ||
devalue({ | ||
object: { | ||
array: [map] | ||
} | ||
}) | ||
} catch (e) { | ||
console.log(e.path); // '.object.array[0].get("key")' | ||
} | ||
``` | ||
## XSS mitigation | ||
@@ -44,0 +61,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
20342
483
139
1
2