From 078fa09ff4782523cf404b5318882a4358ea5d23 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:22:12 +0200 Subject: [PATCH] Upstream davidmarkclements/rfdc state of the touched files before 38fddd8c01 --- index.js | 198 ++++++++++++++++++++++++++++++++ test/index.js | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 index.js create mode 100644 test/index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..a964561 --- /dev/null +++ b/index.js @@ -0,0 +1,198 @@ +'use strict' +module.exports = rfdc + +function copyBuffer (cur) { + if (cur instanceof Buffer) { + return Buffer.from(cur) + } + + return new cur.constructor(cur.buffer.slice(), cur.byteOffset, cur.length) +} + +function rfdc (opts) { + opts = opts || {} + if (opts.circles) return rfdcCircles(opts) + + const constructorHandlers = new Map() + constructorHandlers.set(Date, (o) => new Date(o)) + constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) + constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) + if (opts.constructorHandlers) { + for (const handler of opts.constructorHandlers) { + constructorHandlers.set(handler[0], handler[1]) + } + } + + let handler = null + + return opts.proto ? cloneProto : clone + + function cloneArray (a, fn) { + const keys = Object.keys(a) + const a2 = new Array(keys.length) + for (let i = 0; i < keys.length; i++) { + const k = keys[i] + const cur = a[k] + if (typeof cur !== 'object' || cur === null) { + a2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + a2[k] = handler(cur, fn) + } else if (ArrayBuffer.isView(cur)) { + a2[k] = copyBuffer(cur) + } else { + a2[k] = fn(cur) + } + } + return a2 + } + + function clone (o) { + if (typeof o !== 'object' || o === null) return o + if (Array.isArray(o)) return cloneArray(o, clone) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, clone) + } + const o2 = {} + for (const k in o) { + if (Object.hasOwnProperty.call(o, k) === false) continue + const cur = o[k] + if (typeof cur !== 'object' || cur === null) { + o2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, clone) + } else if (ArrayBuffer.isView(cur)) { + o2[k] = copyBuffer(cur) + } else { + o2[k] = clone(cur) + } + } + return o2 + } + + function cloneProto (o) { + if (typeof o !== 'object' || o === null) return o + if (Array.isArray(o)) return cloneArray(o, cloneProto) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, cloneProto) + } + const o2 = {} + for (const k in o) { + const cur = o[k] + if (typeof cur !== 'object' || cur === null) { + o2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, cloneProto) + } else if (ArrayBuffer.isView(cur)) { + o2[k] = copyBuffer(cur) + } else { + o2[k] = cloneProto(cur) + } + } + return o2 + } +} + +function rfdcCircles (opts) { + const refs = [] + const refsNew = [] + + const constructorHandlers = new Map() + constructorHandlers.set(Date, (o) => new Date(o)) + constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))) + constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))) + if (opts.constructorHandlers) { + for (const handler of opts.constructorHandlers) { + constructorHandlers.set(handler[0], handler[1]) + } + } + + let handler = null + return opts.proto ? cloneProto : clone + + function cloneArray (a, fn) { + const keys = Object.keys(a) + const a2 = new Array(keys.length) + for (let i = 0; i < keys.length; i++) { + const k = keys[i] + const cur = a[k] + if (typeof cur !== 'object' || cur === null) { + a2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + a2[k] = handler(cur, fn) + } else if (ArrayBuffer.isView(cur)) { + a2[k] = copyBuffer(cur) + } else { + const index = refs.indexOf(cur) + if (index !== -1) { + a2[k] = refsNew[index] + } else { + a2[k] = fn(cur) + } + } + } + return a2 + } + + function clone (o) { + if (typeof o !== 'object' || o === null) return o + if (Array.isArray(o)) return cloneArray(o, clone) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, clone) + } + const o2 = {} + refs.push(o) + refsNew.push(o2) + for (const k in o) { + if (Object.hasOwnProperty.call(o, k) === false) continue + const cur = o[k] + if (typeof cur !== 'object' || cur === null) { + o2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, clone) + } else if (ArrayBuffer.isView(cur)) { + o2[k] = copyBuffer(cur) + } else { + const i = refs.indexOf(cur) + if (i !== -1) { + o2[k] = refsNew[i] + } else { + o2[k] = clone(cur) + } + } + } + refs.pop() + refsNew.pop() + return o2 + } + + function cloneProto (o) { + if (typeof o !== 'object' || o === null) return o + if (Array.isArray(o)) return cloneArray(o, cloneProto) + if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) { + return handler(o, cloneProto) + } + const o2 = {} + refs.push(o) + refsNew.push(o2) + for (const k in o) { + const cur = o[k] + if (typeof cur !== 'object' || cur === null) { + o2[k] = cur + } else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) { + o2[k] = handler(cur, cloneProto) + } else if (ArrayBuffer.isView(cur)) { + o2[k] = copyBuffer(cur) + } else { + const i = refs.indexOf(cur) + if (i !== -1) { + o2[k] = refsNew[i] + } else { + o2[k] = cloneProto(cur) + } + } + } + refs.pop() + refsNew.pop() + return o2 + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..5bdd9ac --- /dev/null +++ b/test/index.js @@ -0,0 +1,306 @@ +'use strict' + +const { test } = require('tap') +const rfdc = require('..') +const cloneDefault = require('../default') +const clone = rfdc() +const cloneProto = rfdc({ proto: true }) +const cloneCircles = rfdc({ circles: true }) +const cloneCirclesProto = rfdc({ circles: true, proto: true }) + +const rnd = (max) => Math.round(Math.random() * max) + +types(clone, 'default') +types(cloneProto, 'proto option') +types(cloneCircles, 'circles option') +types(cloneCirclesProto, 'circles and proto option') + +test('default – does not copy proto properties', async ({ is }) => { + is(clone(Object.create({ a: 1 })).a, undefined, 'value not copied') +}) +test('default – shorthand import', async ({ same }) => { + same( + clone(Object.create({ a: 1 })), + cloneDefault(Object.create({ a: 1 })), + 'import equals clone with default options' + ) +}) +test('proto option – copies enumerable proto properties', async ({ is }) => { + is(cloneProto(Object.create({ a: 1 })).a, 1, 'value copied') +}) +test('circles option - circular object', async ({ same, is, isNot }) => { + const o = { nest: { a: 1, b: 2 } } + o.circular = o + same(cloneCircles(o), o, 'same values') + isNot(cloneCircles(o), o, 'different objects') + isNot(cloneCircles(o).nest, o.nest, 'different nested objects') + const c = cloneCircles(o) + is(c.circular, c, 'circular references point to copied parent') + isNot(c.circular, o, 'circular references do not point to original parent') +}) +test('circles option – deep circular object', async ({ same, is, isNot }) => { + const o = { nest: { a: 1, b: 2 } } + o.nest.circular = o + same(cloneCircles(o), o, 'same values') + isNot(cloneCircles(o), o, 'different objects') + isNot(cloneCircles(o).nest, o.nest, 'different nested objects') + const c = cloneCircles(o) + is(c.nest.circular, c, 'circular references point to copied parent') + isNot( + c.nest.circular, + o, + 'circular references do not point to original parent' + ) +}) +test('circles option alone – does not copy proto properties', async ({ + is +}) => { + is(cloneCircles(Object.create({ a: 1 })).a, undefined, 'value not copied') +}) +test('circles and proto option – copies enumerable proto properties', async ({ + is +}) => { + is(cloneCirclesProto(Object.create({ a: 1 })).a, 1, 'value copied') +}) +test('circles and proto option - circular object', async ({ + same, + is, + isNot +}) => { + const o = { nest: { a: 1, b: 2 } } + o.circular = o + same(cloneCirclesProto(o), o, 'same values') + isNot(cloneCirclesProto(o), o, 'different objects') + isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') + const c = cloneCirclesProto(o) + is(c.circular, c, 'circular references point to copied parent') + isNot(c.circular, o, 'circular references do not point to original parent') +}) +test('circles and proto option – deep circular object', async ({ + same, + is, + isNot +}) => { + const o = { nest: { a: 1, b: 2 } } + o.nest.circular = o + same(cloneCirclesProto(o), o, 'same values') + isNot(cloneCirclesProto(o), o, 'different objects') + isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') + const c = cloneCirclesProto(o) + is(c.nest.circular, c, 'circular references point to copied parent') + isNot( + c.nest.circular, + o, + 'circular references do not point to original parent' + ) +}) +test('circles and proto option – deep circular array', async ({ + same, + is, + isNot +}) => { + const o = { nest: [1, 2] } + o.nest.push(o) + same(cloneCirclesProto(o), o, 'same values') + isNot(cloneCirclesProto(o), o, 'different objects') + isNot(cloneCirclesProto(o).nest, o.nest, 'different nested objects') + const c = cloneCirclesProto(o) + is(c.nest[2], c, 'circular references point to copied parent') + isNot(c.nest[2], o, 'circular references do not point to original parent') +}) +test('custom constructor handler', async ({ same, ok, isNot }) => { + class Foo { + constructor (s) { + this.s = s + } + } + const data = { foo: new Foo('foo') } + const cloned = rfdc({ constructorHandlers: [[Foo, (o) => new Foo(o.s)]] })(data) + ok(cloned.foo instanceof Foo) + same(cloned.foo.s, data.foo.s, 'same values') + isNot(cloned.foo, data.foo, 'different objects') +}) +test('custom RegExp handler', async ({ same, ok, isNot }) => { + const data = { regex: /foo/ } + const cloned = rfdc({ constructorHandlers: [[RegExp, (o) => new RegExp(o)]] })(data) + isNot(cloned.regex, data.regex, 'different objects') + ok(cloned.regex.test('foo')) +}) + +function types (clone, label) { + test(label + ' – number', async ({ is }) => { + is(clone(42), 42, 'same value') + }) + test(label + ' – string', async ({ is }) => { + is(clone('str'), 'str', 'same value') + }) + test(label + ' – boolean', async ({ is }) => { + is(clone(true), true, 'same value') + }) + test(label + ' – function', async ({ is }) => { + const fn = () => {} + is(clone(fn), fn, 'same function') + }) + test(label + ' – async function', async ({ is }) => { + const fn = async () => {} + is(clone(fn), fn, 'same function') + }) + test(label + ' – generator function', async ({ is }) => { + const fn = function * () {} + is(clone(fn), fn, 'same function') + }) + test(label + ' – date', async ({ is, isNot }) => { + const date = new Date() + is(+clone(date), +date, 'same value') + isNot(clone(date), date, 'different object') + }) + test(label + ' – null', async ({ is }) => { + is(clone(null), null, 'same value') + }) + test(label + ' – shallow object', async ({ same, isNot }) => { + const o = { a: 1, b: 2 } + same(clone(o), o, 'same values') + isNot(clone(o), o, 'different object') + }) + test(label + ' – shallow array', async ({ same, isNot }) => { + const o = [1, 2] + same(clone(o), o, 'same values') + isNot(clone(o), o, 'different arrays') + }) + test(label + ' – deep object', async ({ same, isNot }) => { + const o = { nest: { a: 1, b: 2 } } + same(clone(o), o, 'same values') + isNot(clone(o), o, 'different objects') + isNot(clone(o).nest, o.nest, 'different nested objects') + }) + test(label + ' – deep array', async ({ same, isNot }) => { + const o = [{ a: 1, b: 2 }, [3]] + same(clone(o), o, 'same values') + isNot(clone(o), o, 'different arrays') + isNot(clone(o)[0], o[0], 'different array elements') + isNot(clone(o)[1], o[1], 'different array elements') + }) + test(label + ' – nested number', async ({ is }) => { + is(clone({ a: 1 }).a, 1, 'same value') + }) + test(label + ' – nested string', async ({ is }) => { + is(clone({ s: 'str' }).s, 'str', 'same value') + }) + test(label + ' – nested boolean', async ({ is }) => { + is(clone({ b: true }).b, true, 'same value') + }) + test(label + ' – nested function', async ({ is }) => { + const fn = () => {} + is(clone({ fn }).fn, fn, 'same function') + }) + test(label + ' – nested async function', async ({ is }) => { + const fn = async () => {} + is(clone({ fn }).fn, fn, 'same function') + }) + test(label + ' – nested generator function', async ({ is }) => { + const fn = function * () {} + is(clone({ fn }).fn, fn, 'same function') + }) + test(label + ' – nested date', async ({ is, isNot }) => { + const date = new Date() + is(+clone({ d: date }).d, +date, 'same value') + isNot(clone({ d: date }).d, date, 'different object') + }) + test(label + ' – nested date in array', async ({ is, isNot }) => { + const date = new Date() + is(+clone({ d: [date] }).d[0], +date, 'same value') + isNot(clone({ d: [date] }).d[0], date, 'different object') + is(+cloneCircles({ d: [date] }).d[0], +date, 'same value') + isNot(cloneCircles({ d: [date] }).d, date, 'different object') + }) + test(label + ' – nested null', async ({ is }) => { + is(clone({ n: null }).n, null, 'same value') + }) + test(label + ' – arguments', async ({ isNot, same }) => { + function fn (...args) { + same(clone(arguments), args, 'same values') + isNot(clone(arguments), arguments, 'different object') + } + fn(1, 2, 3) + }) + test(`${label} copies buffers from object correctly`, async ({ ok, is, isNot }) => { + const input = Date.now().toString(36) + const inputBuffer = Buffer.from(input) + const clonedBuffer = clone({ a: inputBuffer }).a + ok(Buffer.isBuffer(clonedBuffer), 'cloned value is buffer') + isNot(clonedBuffer, inputBuffer, 'cloned buffer is not same as input buffer') + is(clonedBuffer.toString(), input, 'cloned buffer content is correct') + }) + test(`${label} copies buffers from arrays correctly`, async ({ ok, is, isNot }) => { + const input = Date.now().toString(36) + const inputBuffer = Buffer.from(input) + const [clonedBuffer] = clone([inputBuffer]) + ok(Buffer.isBuffer(clonedBuffer), 'cloned value is buffer') + isNot(clonedBuffer, inputBuffer, 'cloned buffer is not same as input buffer') + is(clonedBuffer.toString(), input, 'cloned buffer content is correct') + }) + test(`${label} copies TypedArrays from object correctly`, async ({ ok, is, isNot }) => { + const [input1, input2] = [rnd(10), rnd(10)] + const buffer = new ArrayBuffer(8) + const int32View = new Int32Array(buffer) + int32View[0] = input1 + int32View[1] = input2 + const cloned = clone({ a: int32View }).a + ok(cloned instanceof Int32Array, 'cloned value is instance of class') + isNot(cloned, int32View, 'cloned value is not same as input value') + is(cloned[0], input1, 'cloned value content is correct') + is(cloned[1], input2, 'cloned value content is correct') + }) + test(`${label} copies TypedArrays from array correctly`, async ({ ok, is, isNot }) => { + const [input1, input2] = [rnd(10), rnd(10)] + const buffer = new ArrayBuffer(16) + const int32View = new Int32Array(buffer) + int32View[0] = input1 + int32View[1] = input2 + const [cloned] = clone([int32View]) + ok(cloned instanceof Int32Array, 'cloned value is instance of class') + isNot(cloned, int32View, 'cloned value is not same as input value') + is(cloned[0], input1, 'cloned value content is correct') + is(cloned[1], input2, 'cloned value content is correct') + }) + test(`${label} copies complex TypedArrays`, async ({ ok, deepEqual, is, isNot }) => { + const [input1, input2, input3] = [rnd(10), rnd(10), rnd(10)] + const buffer = new ArrayBuffer(4) + const view1 = new Int8Array(buffer, 0, 2) + const view2 = new Int8Array(buffer, 2, 2) + const view3 = new Int8Array(buffer) + view1[0] = input1 + view2[0] = input2 + view3[3] = input3 + const cloned = clone({ view1, view2, view3 }) + ok(cloned.view1 instanceof Int8Array, 'cloned value is instance of class') + ok(cloned.view2 instanceof Int8Array, 'cloned value is instance of class') + ok(cloned.view3 instanceof Int8Array, 'cloned value is instance of class') + isNot(cloned.view1, view1, 'cloned value is not same as input value') + isNot(cloned.view2, view2, 'cloned value is not same as input value') + isNot(cloned.view3, view3, 'cloned value is not same as input value') + deepEqual(Array.from(cloned.view1), [input1, 0], 'cloned value content is correct') + deepEqual(Array.from(cloned.view2), [input2, input3], 'cloned value content is correct') + deepEqual(Array.from(cloned.view3), [input1, 0, input2, input3], 'cloned value content is correct') + }) + test(`${label} - maps`, async ({ same, isNot }) => { + const map = new Map([['a', 1]]) + same(Array.from(clone(map)), [['a', 1]], 'same value') + isNot(clone(map), map, 'different object') + }) + test(`${label} - sets`, async ({ same, isNot }) => { + const set = new Set([1]) + same(Array.from(clone(set)), [1]) + isNot(clone(set), set, 'different object') + }) + test(`${label} - nested maps`, async ({ same, isNot }) => { + const data = { m: new Map([['a', 1]]) } + same(Array.from(clone(data).m), [['a', 1]], 'same value') + isNot(clone(data).m, data.m, 'different object') + }) + test(`${label} - nested sets`, async ({ same, isNot }) => { + const data = { s: new Set([1]) } + same(Array.from(clone(data).s), [1], 'same value') + isNot(clone(data).s, data.s, 'different object') + }) +}