uneval
v0.4.0
Published
Convert a JS value to JS source code, like eval in reverse.
Maintainers
Readme
Features
undefinedandnullbooleannumber(including-0)stringandRegExp(including unpaired surrogates,</script>escaping, etc.)- Boxed primitives
BigInt- Shared
and
well-known
Symbol Array(including sparse arrays)Object(includingnull-prototype, arbitrary descriptors, etc.)SetMapDate(including invalid ones)TemporalURLandURLSearchParamsArrayBuffer(includingresizable,detached, andmaxByteLength)- Node
Buffer TypedArray(includingBigIntarrays,Float16Array, and float arrays with non-canonical NaNs)DataViewargumentsobject (both sloppy and strict mode)Error(including subclasses)- Shared/circular reference (for all of the above types)
- Custom type
Install
$ npm i unevalSpecial thanks to Chakrit Wichian for donating the package name!
Usage
import assert from 'node:assert'
import uneval from 'uneval'
const object = { message: `hello world` }
const source = uneval(object)
console.log(source)
//=> {message:"hello world"}
const roundtrippedObject = (0, eval)(`(${source})`)
assert.deepEqual(roundtrippedObject, object)
const circularObject = {}
circularObject.self = circularObject
const circularSource = uneval(circularObject)
console.log(circularSource)
//=> (a=>a.self=a)({})
const roundtrippedCircularObject = (0, eval)(`(${circularSource})`)
assert.deepEqual(roundtrippedCircularObject, circularObject)Customization
[!WARNING]
We cannot ensure our security guarantees when the
customoption is used.
uneval accepts a custom callback for unevaling values.
import assert from 'node:assert'
import uneval from 'uneval'
class Person {
constructor(name) {
this.name = name
}
}
const people = {
tomer: new Person(`Tomer`),
amanda: new Person(`Amanda`),
}
const source = uneval(people, {
custom: (value, uneval) => {
if (value instanceof Person) {
return `new Person(${uneval(value.name)})`
}
},
})
console.log(source)
//=> {tomer:new Person("Tomer"),amanda:new Person("Amanda")}
const roundtrippedPeople = (0, eval)(`(${source})`)
assert.deepEqual(roundtrippedPeople, people)Return the following types depending on the desired behavior:
stringto provide custom source for the input valuenullto omit the input value from the output (e.g. in arrays, objects, Sets, Maps). Omitting the root value throws anError.undefined(or don't return anything, which is equivalent) to use the default behavior for the input value
The callback can be used to uneval already supported values differently or to
uneval unsupported values such as functions. It also receives a second uneval
param, which is the same uneval function the options were passed to. You can
use it to delegate back to uneval for sub-values.
[!NOTE]
customis only called for each logical value. It is not called for sub-values that are incidentallyunevaled as part ofunevaling a value.Some examples:
customis called for each element of anArraybecause anArrayis a container and its elements are its logical sub-values.customis called for eachDatevalue, but not for aDatevalue's underlying numerical timestamp because theDateitself is the logical value whilenew Date(numericalTimestamp)is just one way aDatecould beunevaled. Other ways are possible, which means the numerical timestamp in the source is just an implementation detail, not a logical sub-value.customis called for eachArrayBuffervalue even when it's nested within aTypedArrayor NodeBufferbecause the underlyingArrayBufferis the logical sub-value backing these parent values. The fact that it can be shared betweenTypedArrayand/or NodeBufferinstances is proof of this.This principle is a bit hand-wavy, but we use our best judgement. If you find a scenario where
customdoesn't work the way you expect, then create an issue.
Priorities
We prioritize these metrics in the following order:
- Security (see our guarantees)
- Correctness (i.e.
(0, eval)(`(${uneval(value)})`)roundtrips) - Generated source size (human-readable output is a non-goal)
- Generated source runtime performance
unevalruntime performanceunevalbundle size
Note that we do still care about metrics lower on the list. We just care about other metrics more.
Security guarantees
The following are safe UNLESS custom is used:
Running
unevalon untrusted input.Running
(0, eval)(`(${uneval(value)})`).Embedding
uneval(value)in JS source code, including inside an HTML<script>tag.We always escape
</script>to avoid the following XSS attack:const value = { untrustedInput: `</script><script src='https://evil.com/hacked.js'>`, } const html = ` <script> var preloaded = ${uneval(value)}; </script> `Without escaping, we'd end up with this (after formatting):
<script> var preloaded = {untrustedInput:" </script> <!-- Oh no! We've loaded an evil script :( --> <script src="https://evil.com/hacked.js"> "} </script>But with escaping we get:
<script> var preloaded = { untrustedInput: "<\u002fscript><script src='https://evil.com/hacked.js'>", } </script>Running
unevalon a pooledBuffer.unevaling the underlyingArrayBufferof a pooledBufferand using it in the output source would be more correct from a roundtripping perspective, but wouldn't be safe because the underlyingArrayBuffermay contain sensitive data from otherBuffers.Instead, a
Bufferisunevaled with a slicedArrayBuffercontaining only the data in its view if theArrayBuffer's size matchesBuffer.poolSize. This results in false-positives forBuffers containingArrayBuffers that coincidentally have a size ofBuffer.poolSize.To force an
ArrayBufferto beunevaled as-is, include it elsewhere in the input to signal that it's expected to be in the output. For example:const poolSizeArrayBuffer = new ArrayBuffer(Buffer.poolSize) const source = uneval([ poolSizeArrayBuffer, Buffer.from(poolSizeArrayBuffer, 5, 3), ]) console.log(source) //=> (a=>[a,Buffer.from(a,5,3)])(new ArrayBuffer(8192))
Comparison
The comparison table below is auto-generated by running the full test suite of roundtrip tests against each package.
Each roundtrip test unevals the input value, evals the returned source, and
asserts the eval result is equal to the original input value.
We do not assert each package's source output because it can reasonably differ between packages while still roundtripping.
Emoji key:
| Emoji | Meaning | | :---: | ------------------------ | | ✅ | 100% of tests passing | | 🟢 | 75%–99% of tests passing | | 🟡 | 50%–74% of tests passing | | 🟠 | 25%–49% of tests passing | | 🔴 | 1%–24% of tests passing | | ❌ | 0% of tests passing |
