diff --git a/README.md b/README.md index 2519293..8dd5f3a 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,9 @@ Convert a value to an [ESTree](https://github.com/estree/estree) node. - `instanceAsObject` (boolean, default: `false`) — If true, treat objects that have a prototype as plain objects. +- `preserveReferences` (boolean, default: `false`) — If true, preserve references to the same object + found within the input. This also allows to serialize recursive structures. If needed, the + resulting expression will be an iife. ## Compatibility diff --git a/fixtures/array-multi-reference/input.js b/fixtures/array-multi-reference/input.js new file mode 100644 index 0000000..4d00b30 --- /dev/null +++ b/fixtures/array-multi-reference/input.js @@ -0,0 +1,20 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = new Date(3) + return [1, 'Hello', null, new Date(1), new Date(2), $0, $0] +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = [ + 1, + 'Hello', + null, + new Date(1), + new Date(2), + new Date(3), + new Date(3) +] diff --git a/fixtures/array-recursive-nested-deep/input.js b/fixtures/array-recursive-nested-deep/input.js new file mode 100644 index 0000000..25b0bef --- /dev/null +++ b/fixtures/array-recursive-nested-deep/input.js @@ -0,0 +1,13 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = ['variable 1', , ,], + $0 = ['variable 0', ['variable 2', $1]] + return ($1[1] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/array-recursive-nested/input.js b/fixtures/array-recursive-nested/input.js new file mode 100644 index 0000000..44d92a1 --- /dev/null +++ b/fixtures/array-recursive-nested/input.js @@ -0,0 +1,13 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = ['variable 1', ,], + $0 = ['variable 0', $1] + return ($1[1] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/array-recursive/input.js b/fixtures/array-recursive/input.js new file mode 100644 index 0000000..f96b6ba --- /dev/null +++ b/fixtures/array-recursive/input.js @@ -0,0 +1,12 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = ['recursive', , ,] + return ($0[1] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/array/input.js b/fixtures/array/input.js index fd2ca99..f0c6ce3 100644 --- a/fixtures/array/input.js +++ b/fixtures/array/input.js @@ -1 +1,9 @@ -export default [1, '2', , undefined] +// Used as input +// { preserveReferences: true } +export default [1, '2', , {}, ,] + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = [1, '2', , {}, ,] diff --git a/fixtures/big-int/input.js b/fixtures/big-int/input.js index 2758088..1b5c050 100644 --- a/fixtures/big-int/input.js +++ b/fixtures/big-int/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default 1337n + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = 1337n diff --git a/fixtures/big-int64array/input.js b/fixtures/big-int64array/input.js index 397ccbb..17b3656 100644 --- a/fixtures/big-int64array/input.js +++ b/fixtures/big-int64array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new BigInt64Array([1n, 2n, 3n]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new BigInt64Array([1n, 2n, 3n]) diff --git a/fixtures/big-uint64array/input.js b/fixtures/big-uint64array/input.js index deaedb9..aee69ae 100644 --- a/fixtures/big-uint64array/input.js +++ b/fixtures/big-uint64array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new BigUint64Array([1n, 2n, 3n]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new BigUint64Array([1n, 2n, 3n]) diff --git a/fixtures/buffer/input.js b/fixtures/buffer/input.js index 88d5214..60a1af5 100644 --- a/fixtures/buffer/input.js +++ b/fixtures/buffer/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default Buffer.from([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = Buffer.from([1, 2, 3]) diff --git a/fixtures/date/input.js b/fixtures/date/input.js index 0d3bb85..423d80d 100644 --- a/fixtures/date/input.js +++ b/fixtures/date/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Date(1234567890123) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Date(1234567890123) diff --git a/fixtures/false-object/input.js b/fixtures/false-object/input.js index ba64302..ed42476 100644 --- a/fixtures/false-object/input.js +++ b/fixtures/false-object/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Boolean(false) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Boolean(false) diff --git a/fixtures/false/input.js b/fixtures/false/input.js index 2693369..a8aa015 100644 --- a/fixtures/false/input.js +++ b/fixtures/false/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default false + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = false diff --git a/fixtures/float32array/input.js b/fixtures/float32array/input.js index f31c88c..dea8c5d 100644 --- a/fixtures/float32array/input.js +++ b/fixtures/float32array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Float32Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Float32Array([1, 2, 3]) diff --git a/fixtures/float64array/input.js b/fixtures/float64array/input.js index ae497f0..35fa6d5 100644 --- a/fixtures/float64array/input.js +++ b/fixtures/float64array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Float64Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Float64Array([1, 2, 3]) diff --git a/fixtures/infinity/input.js b/fixtures/infinity/input.js index 4d3da23..ee14f6a 100644 --- a/fixtures/infinity/input.js +++ b/fixtures/infinity/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default Infinity + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = Infinity diff --git a/fixtures/int16array/input.js b/fixtures/int16array/input.js index c2aabb2..6c6d48c 100644 --- a/fixtures/int16array/input.js +++ b/fixtures/int16array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Int16Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Int16Array([1, 2, 3]) diff --git a/fixtures/int32array/input.js b/fixtures/int32array/input.js index 7c901a2..01a4b3c 100644 --- a/fixtures/int32array/input.js +++ b/fixtures/int32array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Int32Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Int32Array([1, 2, 3]) diff --git a/fixtures/int8array/input.js b/fixtures/int8array/input.js index 16d3648..c20f873 100644 --- a/fixtures/int8array/input.js +++ b/fixtures/int8array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Int8Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Int8Array([1, 2, 3]) diff --git a/fixtures/map-multi-reference/input.js b/fixtures/map-multi-reference/input.js new file mode 100644 index 0000000..55b2326 --- /dev/null +++ b/fixtures/map-multi-reference/input.js @@ -0,0 +1,25 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = {}, + $1 = {} + return new Map([ + ['key', {}], + [{}, 'value'], + [$0, 42], + [42, $0], + [$1, $1] + ]) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Map([ + ['key', {}], + [{}, 'value'], + [{}, 42], + [42, {}], + [{}, {}] +]) diff --git a/fixtures/map-recursive-deep/input.js b/fixtures/map-recursive-deep/input.js new file mode 100644 index 0000000..9656178 --- /dev/null +++ b/fixtures/map-recursive-deep/input.js @@ -0,0 +1,18 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = new Map(), + $2 = new Map(), + $0 = new Map([ + ['recursive', 'value'], + ['key', $1], + [$2, 'value'] + ]) + return $1.set('key', $0), $2.set($0, 'value'), $0 +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/map-recursive/input.js b/fixtures/map-recursive/input.js new file mode 100644 index 0000000..bbbf3fa --- /dev/null +++ b/fixtures/map-recursive/input.js @@ -0,0 +1,12 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = new Map([['recursive', 'value']]) + return $0.set('key', $0).set($0, 'value') +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/map/input.js b/fixtures/map/input.js index 6362b5f..d6e522b 100644 --- a/fixtures/map/input.js +++ b/fixtures/map/input.js @@ -1,4 +1,15 @@ +// Used as input +// { preserveReferences: true } export default new Map([ [{}, 42], [42, {}] ]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Map([ + [{}, 42], + [42, {}] +]) diff --git a/fixtures/nan/input.js b/fixtures/nan/input.js index d24147e..0554d8d 100644 --- a/fixtures/nan/input.js +++ b/fixtures/nan/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default NaN + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = NaN diff --git a/fixtures/negative-bigint/input.js b/fixtures/negative-bigint/input.js index fc315e5..75ba01d 100644 --- a/fixtures/negative-bigint/input.js +++ b/fixtures/negative-bigint/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default -7n + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = -7n diff --git a/fixtures/negative-infinity/input.js b/fixtures/negative-infinity/input.js index 36ee29d..a1e8390 100644 --- a/fixtures/negative-infinity/input.js +++ b/fixtures/negative-infinity/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default -Infinity + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = -Infinity diff --git a/fixtures/negative-number-object/input.js b/fixtures/negative-number-object/input.js index abb44bd..91988ad 100644 --- a/fixtures/negative-number-object/input.js +++ b/fixtures/negative-number-object/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Number(-456) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Number(-456) diff --git a/fixtures/negative-number/input.js b/fixtures/negative-number/input.js index dd7f346..7f9ff97 100644 --- a/fixtures/negative-number/input.js +++ b/fixtures/negative-number/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default -37 + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = -37 diff --git a/fixtures/negative-zero/input.js b/fixtures/negative-zero/input.js index 340ba2d..df77c86 100644 --- a/fixtures/negative-zero/input.js +++ b/fixtures/negative-zero/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default -0 + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = -0 diff --git a/fixtures/null/input.js b/fixtures/null/input.js index 7b85954..dba8efc 100644 --- a/fixtures/null/input.js +++ b/fixtures/null/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default null + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = null diff --git a/fixtures/number-object/input.js b/fixtures/number-object/input.js index 850da48..ed9875e 100644 --- a/fixtures/number-object/input.js +++ b/fixtures/number-object/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Number(123) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Number(123) diff --git a/fixtures/number/input.js b/fixtures/number/input.js index 02f8a32..3118fae 100644 --- a/fixtures/number/input.js +++ b/fixtures/number/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default 42 + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = 42 diff --git a/fixtures/object-multi-reference/input.js b/fixtures/object-multi-reference/input.js new file mode 100644 index 0000000..873b156 --- /dev/null +++ b/fixtures/object-multi-reference/input.js @@ -0,0 +1,30 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = new Date(3) + return { + number: 1, + string: 'Hello', + nothing: null, + 'new Date(1)': new Date(1), + 'new Date(2)': new Date(2), + 'new Date(3)': $0, + 'also new Date(3)': $0, + [Symbol.for('key')]: 'value' + } +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = { + number: 1, + string: 'Hello', + nothing: null, + 'new Date(1)': new Date(1), + 'new Date(2)': new Date(2), + 'new Date(3)': new Date(3), + 'also new Date(3)': new Date(3), + [Symbol.for('key')]: 'value' +} diff --git a/fixtures/object-null-proto/input.js b/fixtures/object-null-proto/input.js index c9753a5..76e99ec 100644 --- a/fixtures/object-null-proto/input.js +++ b/fixtures/object-null-proto/input.js @@ -1,3 +1,5 @@ +// Used as input +// { preserveReferences: true } export default { __proto__: null, number: 1, @@ -5,3 +7,15 @@ export default { nothing: null, [Symbol.for('key')]: 'value' } + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = { + __proto__: null, + number: 1, + string: 'Hello', + nothing: null, + [Symbol.for('key')]: 'value' +} diff --git a/fixtures/object-recursive-nested-deep/input.js b/fixtures/object-recursive-nested-deep/input.js new file mode 100644 index 0000000..d1e9ed1 --- /dev/null +++ b/fixtures/object-recursive-nested-deep/input.js @@ -0,0 +1,21 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = { + name: 'variable 1' + }, + $0 = { + name: 'variable 0', + $2: { + name: 'variable 2', + $1: $1 + } + } + return ($1['variable 0'] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/object-recursive-nested/input.js b/fixtures/object-recursive-nested/input.js new file mode 100644 index 0000000..1a8d094 --- /dev/null +++ b/fixtures/object-recursive-nested/input.js @@ -0,0 +1,18 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = { + name: 'variable 1' + }, + $0 = { + name: 'variable 0', + $1: $1 + } + return ($1['variable 0'] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/object-recursive/input.js b/fixtures/object-recursive/input.js new file mode 100644 index 0000000..2c7b663 --- /dev/null +++ b/fixtures/object-recursive/input.js @@ -0,0 +1,14 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = { + name: 'recursive' + } + return ($0['resursive'] = $0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/object/input.js b/fixtures/object/input.js index c9f1e35..7d28de2 100644 --- a/fixtures/object/input.js +++ b/fixtures/object/input.js @@ -1,6 +1,23 @@ +// Used as input +// { preserveReferences: true } export default { number: 1, string: 'Hello', nothing: null, + 'new Date(1)': new Date(1), + 'new Date(2)': new Date(2), + [Symbol.for('key')]: 'value' +} + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = { + number: 1, + string: 'Hello', + nothing: null, + 'new Date(1)': new Date(1), + 'new Date(2)': new Date(2), [Symbol.for('key')]: 'value' } diff --git a/fixtures/regexp/input.js b/fixtures/regexp/input.js index 4fc3bf3..be8341b 100644 --- a/fixtures/regexp/input.js +++ b/fixtures/regexp/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default /\\s+/i + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = /\\s+/i diff --git a/fixtures/set-after-reference/input.js b/fixtures/set-after-reference/input.js new file mode 100644 index 0000000..b872532 --- /dev/null +++ b/fixtures/set-after-reference/input.js @@ -0,0 +1,12 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = new Set(['before reference']) + return $0.add($0).add('after reference') +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/set-recursive-nested-deep/input.js b/fixtures/set-recursive-nested-deep/input.js new file mode 100644 index 0000000..59af8d3 --- /dev/null +++ b/fixtures/set-recursive-nested-deep/input.js @@ -0,0 +1,13 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = new Set(['variable 1']), + $0 = new Set(['variable 0', new Set(['variable 2', $1])]) + return $1.add($0), $0 +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/set-recursive-nested/input.js b/fixtures/set-recursive-nested/input.js new file mode 100644 index 0000000..f421815 --- /dev/null +++ b/fixtures/set-recursive-nested/input.js @@ -0,0 +1,13 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $1 = new Set(['variable 1']), + $0 = new Set(['variable 0', $1]) + return $1.add($0), $0 +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/set-recursive/input.js b/fixtures/set-recursive/input.js new file mode 100644 index 0000000..a3f654c --- /dev/null +++ b/fixtures/set-recursive/input.js @@ -0,0 +1,12 @@ +// Used as input +// { preserveReferences: true } +export default (() => { + const $0 = new Set(['recursive']) + return $0.add($0) +})() + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +// Recursive references are not supported witout preserveReferences diff --git a/fixtures/set/input.js b/fixtures/set/input.js index 259ea8d..b57a890 100644 --- a/fixtures/set/input.js +++ b/fixtures/set/input.js @@ -1 +1,9 @@ -export default new Set([42, 'not 42']) +// Used as input +// { preserveReferences: true } +export default new Set([42, 'not 42', new Set()]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Set([42, 'not 42', new Set()]) diff --git a/fixtures/string-object/input.js b/fixtures/string-object/input.js index 7927af6..a8d5beb 100644 --- a/fixtures/string-object/input.js +++ b/fixtures/string-object/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new String('Hello string') + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new String('Hello string') diff --git a/fixtures/string/input.js b/fixtures/string/input.js index d5d7b8c..bf7e957 100644 --- a/fixtures/string/input.js +++ b/fixtures/string/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default 'Hello' + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = 'Hello' diff --git a/fixtures/symbol-for/input.js b/fixtures/symbol-for/input.js index a1aae64..644e204 100644 --- a/fixtures/symbol-for/input.js +++ b/fixtures/symbol-for/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default Symbol.for('global') + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = Symbol.for('global') diff --git a/fixtures/true-object/input.js b/fixtures/true-object/input.js index 22ceb17..0ce197a 100644 --- a/fixtures/true-object/input.js +++ b/fixtures/true-object/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Boolean(true) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Boolean(true) diff --git a/fixtures/true/input.js b/fixtures/true/input.js index 186b120..607228a 100644 --- a/fixtures/true/input.js +++ b/fixtures/true/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default true + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = true diff --git a/fixtures/uint16array/input.js b/fixtures/uint16array/input.js index eb6188f..1ae0ade 100644 --- a/fixtures/uint16array/input.js +++ b/fixtures/uint16array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Uint16Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Uint16Array([1, 2, 3]) diff --git a/fixtures/uint32array/input.js b/fixtures/uint32array/input.js index 20e3a91..4edd32e 100644 --- a/fixtures/uint32array/input.js +++ b/fixtures/uint32array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Uint32Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Uint32Array([1, 2, 3]) diff --git a/fixtures/uint8array/input.js b/fixtures/uint8array/input.js index 6d2b5e8..f71fc0f 100644 --- a/fixtures/uint8array/input.js +++ b/fixtures/uint8array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Uint8Array([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Uint8Array([1, 2, 3]) diff --git a/fixtures/uint8clamped-array/input.js b/fixtures/uint8clamped-array/input.js index 1c7a9af..6e4a9bd 100644 --- a/fixtures/uint8clamped-array/input.js +++ b/fixtures/uint8clamped-array/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new Uint8ClampedArray([1, 2, 3]) + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new Uint8ClampedArray([1, 2, 3]) diff --git a/fixtures/undefined/input.js b/fixtures/undefined/input.js index edb7272..a1617b7 100644 --- a/fixtures/undefined/input.js +++ b/fixtures/undefined/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default undefined + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = undefined diff --git a/fixtures/url-search-params/input.js b/fixtures/url-search-params/input.js index 487ca14..b369906 100644 --- a/fixtures/url-search-params/input.js +++ b/fixtures/url-search-params/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new URLSearchParams('everything=awesome') + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new URLSearchParams('everything=awesome') diff --git a/fixtures/url/input.js b/fixtures/url/input.js index 43a3bd6..6995d20 100644 --- a/fixtures/url/input.js +++ b/fixtures/url/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default new URL('https://example.com/') + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = new URL('https://example.com/') diff --git a/fixtures/zero/input.js b/fixtures/zero/input.js index 029f788..a915f31 100644 --- a/fixtures/zero/input.js +++ b/fixtures/zero/input.js @@ -1 +1,9 @@ +// Used as input +// { preserveReferences: true } export default 0 + +// ------------------------------------------------------------------------------------------------- + +// Default output +// { preserveReferences: false } +const withoutPreserveReferences = 0 diff --git a/src/estree-util-value-to-estree.ts b/src/estree-util-value-to-estree.ts index a3a5537..80821e0 100644 --- a/src/estree-util-value-to-estree.ts +++ b/src/estree-util-value-to-estree.ts @@ -1,4 +1,11 @@ -import { type Expression, type Identifier, type Property } from 'estree' +import { + type ArrayExpression, + type Expression, + type Identifier, + type Property, + type SimpleLiteral, + type VariableDeclarator +} from 'estree' import isPlainObject from 'is-plain-obj' /** @@ -13,6 +20,45 @@ function identifier(name: string): Identifier { return { type: 'Identifier', name } } +/** + * Create an estree literal node for a given value. + * + * @param value + * The value for which to create a literal. + * @returns + * The literal node. + */ +function literal(value: SimpleLiteral['value']): SimpleLiteral { + return { type: 'Literal', value } +} + +/** + * Create an estree call expression on an object member. + * + * @param object + * The object to call the method on. + * @param property + * The name of the method to call. + * @param args + * Arguments to pass to the function call + * @returns + * The call expression node. + */ +function methodCall(object: Expression, property: string, args: Expression[]): Expression { + return { + type: 'CallExpression', + optional: false, + callee: { + type: 'MemberExpression', + computed: false, + optional: false, + object, + property: identifier(property) + }, + arguments: args + } +} + /** * Turn a number or bigint into an estree expression. This handles positive and negative numbers and * bigints as well as special numbers. @@ -40,7 +86,7 @@ function processNumber(number: bigint | number): Expression { return identifier(String(number)) } - return { type: 'Literal', value: number } + return literal(number) } /** @@ -53,14 +99,133 @@ function processNumber(number: bigint | number): Expression { * An estree array expression whose elements match the input numbers. */ function processNumberArray(numbers: Iterable): Expression { - return { type: 'ArrayExpression', elements: Array.from(numbers, processNumber) } + const elements: Expression[] = [] + + for (const value of numbers) { + elements.push(processNumber(value)) + } + + return { type: 'ArrayExpression', elements } +} + +/** + * Check whether a value can be constructed from its string representation. + * + * @param value + * The value to check + * @returns + * Whether or not the value can be constructed from its string representation. + */ +function isStringReconstructable(value: unknown): value is URL | URLSearchParams { + return value instanceof URL || value instanceof URLSearchParams +} + +/** + * Check whether a value can be constructed from its `valueOf()` result. + * + * @param value + * The value to check + * @returns + * Whether or not the value can be constructed from its `valueOf()` result. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +function isValueReconstructable(value: unknown): value is Boolean | Date | Number | String { + return ( + value instanceof Boolean || + value instanceof Date || + value instanceof Number || + value instanceof String + ) +} + +/** + * Check whether a value is a typed array. + * + * @param value + * The value to check + * @returns + * Whether or not the value is a typed array. + */ +function isTypedArray( + value: unknown +): value is + | BigInt64Array + | BigUint64Array + | Float32Array + | Float64Array + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array { + return ( + value instanceof BigInt64Array || + value instanceof BigUint64Array || + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int8Array || + value instanceof Int16Array || + value instanceof Int32Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Uint16Array || + value instanceof Uint32Array + ) +} + +interface Context { + /** + * The number of references to this value. + */ + count: number + + /** + * The variable name used to reference the value. + */ + name?: string + + /** + * Whether or not this value recursively references itself. + */ + recursive: boolean + + /** + * The value this context belongs to. + */ + value: unknown +} + +/** + * Compare two value contexts for sorting them based on reference count. + * + * @param a + * The first context to compare. + * @param b + * The second context to compare. + * @returns + * The count of context a minus the count of context b. + */ +function compareContexts(a: Context, b: Context): number { + return a.count - b.count } export interface Options { /** * If true, treat objects that have a prototype as plain objects. + * + * @default false */ instanceAsObject?: boolean + + /** + * If true, preserve references to the same object found within the input. This also allows to + * serialize recursive structures. If needed, the resulting expression will be an iife. + * + * @default false + */ + preserveReferences?: boolean } /** @@ -74,145 +239,396 @@ export interface Options { * The ESTree node. */ export function valueToEstree(value: unknown, options: Options = {}): Expression { - if (value === undefined) { - return identifier(String(value)) - } + const declarations: VariableDeclarator[] = [] + const stack: unknown[] = [] + const collectedContexts = new Map() + const finalizers: Expression[] = [] + const namedContexts: Context[] = [] + let finalExpression: Expression | undefined - if (value == null || typeof value === 'string' || typeof value === 'boolean') { - return { type: 'Literal', value } - } + /** + * Analyze a value and collect all reference contexts. + * + * @param val + * The value to analyze. + */ + function analyze(val: unknown): undefined { + if (typeof val === 'function') { + throw new TypeError(`Unsupported value: ${val}`) + } - if (typeof value === 'bigint' || typeof value === 'number') { - return processNumber(value) - } + if (typeof val !== 'object') { + return + } - if (typeof value === 'symbol') { - if (value.description && value === Symbol.for(value.description)) { - return { - type: 'CallExpression', - optional: false, - callee: { - type: 'MemberExpression', - computed: false, - optional: false, - object: identifier('Symbol'), - property: identifier('for') - }, - arguments: [valueToEstree(value.description, options)] + if (val == null) { + return + } + + const context = collectedContexts.get(val) + if (context) { + if (options.preserveReferences) { + context.count += 1 + } + if (stack.includes(val)) { + if (!options.preserveReferences) { + throw new Error(`Found recursive value: ${val}`) + } + const parent = stack.at(-1)! + const parentContext = collectedContexts.get(parent)! + parentContext.recursive = true + context.recursive = true } + return } - throw new TypeError(`Only global symbols are supported, got: ${String(value)}`) - } + collectedContexts.set(val, { count: 1, recursive: false, value: val }) - if (Array.isArray(value)) { - const elements: (Expression | null)[] = [] - for (let i = 0; i < value.length; i += 1) { - elements.push(i in value ? valueToEstree(value[i], options) : null) + if (isTypedArray(val)) { + return } - return { type: 'ArrayExpression', elements } - } - if ( - value instanceof Boolean || - value instanceof Date || - value instanceof Number || - value instanceof String - ) { - return { - type: 'NewExpression', - callee: identifier(value.constructor.name), - arguments: [valueToEstree(value.valueOf(), options)] + if (isStringReconstructable(val)) { + return } - } - if (value instanceof RegExp) { - return { - type: 'Literal', - value, - regex: { pattern: value.source, flags: value.flags } + if (isValueReconstructable(val)) { + return } - } - if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { - return { - type: 'CallExpression', - optional: false, - callee: { - type: 'MemberExpression', - computed: false, - optional: false, - object: identifier('Buffer'), - property: identifier('from') - }, - arguments: [processNumberArray(value)] + if (value instanceof RegExp) { + return } - } - if ( - value instanceof BigInt64Array || - value instanceof BigUint64Array || - value instanceof Float32Array || - value instanceof Float64Array || - value instanceof Int8Array || - value instanceof Int16Array || - value instanceof Int32Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Uint16Array || - value instanceof Uint32Array - ) { - return { - type: 'NewExpression', - callee: identifier(value.constructor.name), - arguments: [processNumberArray(value)] + stack.push(val) + if (val instanceof Map) { + for (const pair of val) { + analyze(pair[0]) + analyze(pair[1]) + } + } else if (Array.isArray(val) || val instanceof Set) { + for (const entry of val) { + analyze(entry) + } + } else if (options.instanceAsObject || isPlainObject(val)) { + for (const key of Reflect.ownKeys(val)) { + analyze((val as Record)[key]) + } + } else { + throw new TypeError(`Unsupported value: ${val}`) } + stack.pop() } - if (value instanceof Map || value instanceof Set) { - return { - type: 'NewExpression', - callee: identifier(value.constructor.name), - arguments: [valueToEstree([...value], options)] + /** + * Add a finalizer. A finalizer is an expression needed to reconstruct a value after its initial + * creation. + * + * @param val + * The value returned by the expression. + * @param expression + * The expression used to finalize the reconstruction of a value. + */ + function addFinalizer(val: unknown, expression: Expression | undefined): undefined { + if (expression) { + if (val === value && !finalExpression) { + finalExpression = expression + } else { + finalizers.push(expression) + } } } - if (value instanceof URL || value instanceof URLSearchParams) { - return { - type: 'NewExpression', - callee: identifier(value.constructor.name), - arguments: [valueToEstree(String(value), options)] + /** + * Recursively generate the estree expression needed to reconstruct the value. + * + * @param val + * The value to process. + * @param isDeclaration + * Whether or not this is for a variable declaration. + * @returns + * The estree expression to reconstruct the value. + */ + function generate(val: unknown, isDeclaration?: boolean): Expression { + if (val === undefined) { + return identifier(String(val)) + } + + if (val == null || typeof val === 'string' || typeof val === 'boolean') { + return literal(val) + } + + if (typeof val === 'bigint' || typeof val === 'number') { + return processNumber(val) + } + + if (typeof val === 'symbol') { + if (val.description && val === Symbol.for(val.description)) { + return methodCall(identifier('Symbol'), 'for', [literal(val.description)]) + } + + throw new TypeError(`Only global symbols are supported, got: ${String(val)}`) + } + + const context = collectedContexts.get(val) + if (!isDeclaration && context?.name) { + return identifier(context.name) + } + + if (isValueReconstructable(val)) { + return { + type: 'NewExpression', + callee: identifier(val.constructor.name), + arguments: [generate(val.valueOf())] + } + } + + if (val instanceof RegExp) { + return { + type: 'Literal', + value: val, + regex: { pattern: val.source, flags: val.flags } + } + } + + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) { + return methodCall(identifier('Buffer'), 'from', [processNumberArray(val)]) } - } - if (options.instanceAsObject || isPlainObject(value)) { - const properties = Reflect.ownKeys(value).map((key) => ({ - type: 'Property', - method: false, - shorthand: false, - computed: typeof key !== 'string', - kind: 'init', - key: valueToEstree(key, options), - value: valueToEstree((value as Record)[key], options) - })) - - if (Object.getPrototypeOf(value) == null) { - properties.unshift({ + if (isTypedArray(val)) { + return { + type: 'NewExpression', + callee: identifier(val.constructor.name), + arguments: [processNumberArray(val)] + } + } + + if (isStringReconstructable(val)) { + return { + type: 'NewExpression', + callee: identifier(val.constructor.name), + arguments: [literal(String(val))] + } + } + + if (Array.isArray(val)) { + const elements: (Expression | null)[] = Array.from({ length: val.length }) + + for (let index = 0; index < val.length; index += 1) { + if (!(index in val)) { + elements[index] = null + continue + } + + const child = val[index] + const childContext = collectedContexts.get(child) + if ( + context && + childContext && + namedContexts.indexOf(childContext) >= namedContexts.indexOf(context) + ) { + addFinalizer(child, { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: true, + optional: false, + object: identifier(context.name!), + property: literal(index) + }, + right: identifier(childContext.name!) + }) + } else { + elements[index] = generate(child) + } + } + + return { + type: 'ArrayExpression', + elements + } + } + + if (val instanceof Set) { + const elements: Expression[] = [] + let finalizer: Expression | undefined + + for (const child of val) { + if (finalizer) { + finalizer = methodCall(finalizer, 'add', [generate(child)]) + } else { + const childContext = collectedContexts.get(child) + if ( + context && + childContext && + namedContexts.indexOf(childContext) >= namedContexts.indexOf(context) + ) { + finalizer = methodCall(identifier(context.name!), 'add', [generate(child)]) + } else { + elements.push(generate(child)) + } + } + } + + addFinalizer(val, finalizer) + + return { + type: 'NewExpression', + callee: identifier('Set'), + arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : [] + } + } + + if (val instanceof Map) { + const elements: ArrayExpression[] = [] + let finalizer: Expression | undefined + + for (const [key, item] of val) { + if (finalizer) { + finalizer = methodCall(finalizer, 'set', [generate(key), generate(item)]) + } else { + const keyContext = collectedContexts.get(key) + const itemContext = collectedContexts.get(item) + + if ( + context && + ((keyContext && namedContexts.indexOf(keyContext) >= namedContexts.indexOf(context)) || + (itemContext && namedContexts.indexOf(itemContext) >= namedContexts.indexOf(context))) + ) { + finalizer = methodCall(identifier(context.name!), 'set', [ + generate(key), + generate(item) + ]) + } else { + elements.push({ + type: 'ArrayExpression', + elements: [generate(key), generate(item)] + }) + } + } + } + + addFinalizer(val, finalizer) + + return { + type: 'NewExpression', + callee: identifier('Map'), + arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : [] + } + } + + const properties: Property[] = [] + if (Object.getPrototypeOf(val) == null) { + properties.push({ type: 'Property', method: false, shorthand: false, computed: false, kind: 'init', key: identifier('__proto__'), - value: { type: 'Literal', value: null } + value: literal(null) }) } + const object = val as Record + for (const key of Reflect.ownKeys(val)) { + const computed = typeof key !== 'string' + const keyExpression = generate(key) + const child = object[key] + const childContext = collectedContexts.get(child) + if ( + context && + childContext && + namedContexts.indexOf(childContext) >= namedContexts.indexOf(context) + ) { + addFinalizer(child, { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: true, + optional: false, + object: identifier(context.name!), + property: keyExpression + }, + right: generate(child) + }) + } else { + properties.push({ + type: 'Property', + method: false, + shorthand: false, + computed, + kind: 'init', + key: keyExpression, + value: generate(child) + }) + } + } + return { type: 'ObjectExpression', properties } } - throw new TypeError(`Unsupported value: ${String(value)}`) + analyze(value) + + const rootContext = collectedContexts.get(value)! + for (const [val, context] of collectedContexts) { + if (context.recursive || context.count > 1) { + // Assign reused or recursive references to a variable. + context.name = `$${namedContexts.length}` + namedContexts.push(context) + } else { + // Otherwise don’t treat it as a reference. + collectedContexts.delete(val) + } + } + + if (!namedContexts.length) { + return generate(value) + } + + for (const context of namedContexts.sort(compareContexts)) { + declarations.push({ + type: 'VariableDeclarator', + id: identifier(context.name!), + init: generate(context.value, true) + }) + } + + finalizers.push( + finalExpression || + (rootContext.name ? identifier(rootContext.name) : generate(rootContext.value)) + ) + + return { + type: 'CallExpression', + optional: false, + arguments: [], + callee: { + type: 'ArrowFunctionExpression', + expression: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'VariableDeclaration', + kind: 'const', + declarations + }, + { + type: 'ReturnStatement', + argument: { + type: 'SequenceExpression', + expressions: finalizers + } + } + ] + } + } + } } diff --git a/src/test.ts b/src/test.ts index fb4d324..2dfdbb0 100644 --- a/src/test.ts +++ b/src/test.ts @@ -11,8 +11,24 @@ testFixturesDirectory({ tests: { async 'input.js'(input) { const { default: value } = await import(input.path) - const ast = valueToEstree(value) - return `export default ${generate(ast)}` + const withPreserveReferences = generate(valueToEstree(value, { preserveReferences: true })) + let withoutPreserveReferences: string + try { + withoutPreserveReferences = `const withoutPreserveReferences = ${generate(valueToEstree(value))}` + } catch { + withoutPreserveReferences = + '// Recursive references are not supported witout preserveReferences' + } + return ` + // Used as input + // { preserveReferences: true } + export default ${withPreserveReferences} + + // ------------------------------------------------------------------------------------------------- + + // Default output + // { preserveReferences: false } + ${withoutPreserveReferences}` } } })