diff --git a/.eslintrc.js b/.eslintrc.js index bc24157c47dc..a023f56dfc23 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -108,7 +108,7 @@ const base = { // disallow unnecessary labels 'no-extra-label': ERROR, // disallow fallthrough of case statements - 'no-fallthrough': ERROR, + 'no-fallthrough': [ERROR, { commentPattern: 'break omitted' }], // disallow the use of leading or trailing decimal points in numeric literals 'no-floating-decimal': ERROR, // disallow reassignments of native objects @@ -1119,11 +1119,12 @@ module.exports = { 'tests/compat/**', ], globals: { - compositeKey: READONLY, - compositeSymbol: READONLY, AsyncIterator: READONLY, Iterator: READONLY, Observable: READONLY, + compositeKey: READONLY, + compositeSymbol: READONLY, + structuredClone: READONLY, }, }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 1404606d156d..fac53432fe88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog ##### Unreleased +- Added `structuredClone` method [from the HTML spec](https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone), [see MDN](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) + - Includes all cases of cloning and transferring of required ECMAScript and platform types that can be polyfilled, for the details see [the caveats](https://github.com/zloirock/core-js#caveats-when-using-structuredclone-polyfill) + - Uses native structured cloning algorithm implementations where it's possible + - Includes the new semantic of errors cloning from [`html/5749`](https://github.com/whatwg/html/pull/5749) - Added `DOMException` polyfill, [the Web IDL spec](https://webidl.spec.whatwg.org/#idl-DOMException), [see MDN](https://developer.mozilla.org/en-US/docs/Web/API/DOMException) - Includes `DOMException` and its attributes polyfills with fixes of many different engines bugs - Includes `DOMException#stack` property polyfill in engines that should have it diff --git a/README.md b/README.md index 3aea84f826fe..9b2d270afb13 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Promise.resolve(32).then(x => console.log(x)); // => 32 - [Pre-stage 0 proposals](#pre-stage-0-proposals) - [`Reflect` metadata](#reflect-metadata) - [Web standards](#web-standards) + - [`structuredClone`](#structuredclone) - [`setTimeout` and `setInterval`](#settimeout-and-setinterval) - [`setImmediate`](#setimmediate) - [`queueMicrotask`](#queuemicrotask) @@ -2865,6 +2866,51 @@ Reflect.getOwnMetadata('foo', object); // => 'bar' ```js core-js(-pure)/web ``` +#### `structuredClone`[⬆](#index) +[Spec](https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone), module [`web.structured-clone`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.structured-clone.js) +```js +function structuredClone(value: Serializable, { transfer?: Sequence }): any; +``` +[*CommonJS entry points:*](#commonjs-api) +```js +core-js/web/structured-clone +core-js(-pure)/stable|features/structured-clone +``` +[*Examples*](t.ly/dBEC): +```js +const structured = [{ a: 42 }]; +const sclone = structuredClone(structured); +console.log(sclone); // => [{ a: 42 }] +console.log(structured !== sclone); // => true +console.log(structured[0] !== sclone[0]); // => true + +const circular = {}; +circular.circular = circular; +const cclone = structuredClone(circular); +console.log(cclone.circular === cclone); // => true + +structuredClone(42); // => 42 +structuredClone({ x: 42 }); // => { x: 42 } +structuredClone([1, 2, 3]); // => [1, 2, 3] +structuredClone(new Set([1, 2, 3])); // => Set{ 1, 2, 3 } +structuredClone(new Map([['a', 1], ['b', 2]])); // => Map{ a: 1, b: 2 } +structuredClone(new Int8Array([1, 2, 3])); // => new Int8Array([1, 2, 3]) +structuredClone(new AggregateError([1, 2, 3], 'message')); // => new AggregateError([1, 2, 3], 'message')) +structuredClone(new TypeError('message', { cause: 42 })); // => new TypeError('message', { cause: 42 }) +structuredClone(new DOMException('message', 'DataCloneError')); // => new DOMException('message', 'DataCloneError') +structuredClone(document.getElementById('myfileinput')); // => new FileList +structuredClone(new DOMPoint(1, 2, 3, 4)); // => new DOMPoint(1, 2, 3, 4) +structuredClone(new Blob(['test'])); // => new Blob(['test']) +structuredClone(new ImageData(8, 8)); // => new ImageData(8, 8) +// etc. + +structuredClone(new WeakMap()); // => DataCloneError on non-serializable types +``` +##### Caveats when using `structuredClone` polyfill:[⬆](#index) + +* `ArrayBuffer` instances and many platform types cannot be transferred in most engines since we have no way to polyfill this behavior, however `.transfer` option works for some platform types. I recommend avoiding this option. +* Some specific platform types can't be cloned in old engines. Mainly it's very specific types or very old engines, but here are some exceptions. For example, we have no sync way to clone `ImageBitmap` in Safari 14.0- or Firefox 83-, so it's recommended to look to the [polyfill source](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.structured-clone.js) if you wanna clone something specific. + #### `setTimeout` and `setInterval`[⬆](#index) Module [`web.timers`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.timers.js). Additional arguments fix for IE9-. ```js diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index bda20c3ab54c..691168964a25 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -1850,6 +1850,13 @@ export const data = { node: '12.0', // '11.0', safari: '12.1', }, + 'web.structured-clone': { + // https://github.com/whatwg/html/pull/5749 + // deno: '1.14', + // current FF implementation can't clone errors + // firefox: '94', + // node: '17.0', + }, 'web.timers': { android: '1.5', chrome: '1', diff --git a/packages/core-js-compat/src/modules-by-versions.mjs b/packages/core-js-compat/src/modules-by-versions.mjs index 2440ecd67d3c..b6f0f2dde692 100644 --- a/packages/core-js-compat/src/modules-by-versions.mjs +++ b/packages/core-js-compat/src/modules-by-versions.mjs @@ -133,5 +133,6 @@ export default { 'web.dom-exception.constructor', 'web.dom-exception.stack', 'web.dom-exception.to-string-tag', + 'web.structured-clone', ], }; diff --git a/packages/core-js/es/array-buffer/constructor.js b/packages/core-js/es/array-buffer/constructor.js index b7a76ed5ce70..addb8503ff08 100644 --- a/packages/core-js/es/array-buffer/constructor.js +++ b/packages/core-js/es/array-buffer/constructor.js @@ -1,4 +1,5 @@ require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.object.to-string'); var path = require('../../internals/path'); diff --git a/packages/core-js/es/data-view/index.js b/packages/core-js/es/data-view/index.js index da3582d7ffa2..aa7f63ef61f3 100644 --- a/packages/core-js/es/data-view/index.js +++ b/packages/core-js/es/data-view/index.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.data-view'); require('../../modules/es.object.to-string'); var path = require('../../internals/path'); diff --git a/packages/core-js/es/typed-array/float32-array.js b/packages/core-js/es/typed-array/float32-array.js index 70d1f810fe1d..92880ea2de50 100644 --- a/packages/core-js/es/typed-array/float32-array.js +++ b/packages/core-js/es/typed-array/float32-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.float32-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/float64-array.js b/packages/core-js/es/typed-array/float64-array.js index b84eae0a9cdf..17dafd612413 100644 --- a/packages/core-js/es/typed-array/float64-array.js +++ b/packages/core-js/es/typed-array/float64-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.float64-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/int16-array.js b/packages/core-js/es/typed-array/int16-array.js index 81f5a8e5d038..a2938d2669b4 100644 --- a/packages/core-js/es/typed-array/int16-array.js +++ b/packages/core-js/es/typed-array/int16-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.int16-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/int32-array.js b/packages/core-js/es/typed-array/int32-array.js index 48176bf56a36..5f1d985cbe6b 100644 --- a/packages/core-js/es/typed-array/int32-array.js +++ b/packages/core-js/es/typed-array/int32-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.int32-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/int8-array.js b/packages/core-js/es/typed-array/int8-array.js index 7d53845a78b5..5e8eaa6df297 100644 --- a/packages/core-js/es/typed-array/int8-array.js +++ b/packages/core-js/es/typed-array/int8-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.int8-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/uint16-array.js b/packages/core-js/es/typed-array/uint16-array.js index 9fe6065e5087..152d9af1fc79 100644 --- a/packages/core-js/es/typed-array/uint16-array.js +++ b/packages/core-js/es/typed-array/uint16-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.uint16-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/uint32-array.js b/packages/core-js/es/typed-array/uint32-array.js index 26bdb10c22f1..c1977a846a82 100644 --- a/packages/core-js/es/typed-array/uint32-array.js +++ b/packages/core-js/es/typed-array/uint32-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.uint32-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/uint8-array.js b/packages/core-js/es/typed-array/uint8-array.js index 96a5bf12f982..7b547c19af3e 100644 --- a/packages/core-js/es/typed-array/uint8-array.js +++ b/packages/core-js/es/typed-array/uint8-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.uint8-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/es/typed-array/uint8-clamped-array.js b/packages/core-js/es/typed-array/uint8-clamped-array.js index 7e2d3ce70374..50aea2dc26bc 100644 --- a/packages/core-js/es/typed-array/uint8-clamped-array.js +++ b/packages/core-js/es/typed-array/uint8-clamped-array.js @@ -1,3 +1,5 @@ +require('../../modules/es.array-buffer.constructor'); +require('../../modules/es.array-buffer.slice'); require('../../modules/es.typed-array.uint8-clamped-array'); require('./methods'); var global = require('../../internals/global'); diff --git a/packages/core-js/features/structured-clone.js b/packages/core-js/features/structured-clone.js new file mode 100644 index 000000000000..1b48415eed2d --- /dev/null +++ b/packages/core-js/features/structured-clone.js @@ -0,0 +1,3 @@ +var parent = require('../stable/structured-clone'); + +module.exports = parent; diff --git a/packages/core-js/modules/web.structured-clone.js b/packages/core-js/modules/web.structured-clone.js new file mode 100644 index 000000000000..97146835f022 --- /dev/null +++ b/packages/core-js/modules/web.structured-clone.js @@ -0,0 +1,435 @@ +var IS_PURE = require('../internals/is-pure'); +var $ = require('../internals/export'); +var global = require('../internals/global'); +var getBuiltin = require('../internals/get-built-in'); +var uncurryThis = require('../internals/function-uncurry-this'); +var fails = require('../internals/fails'); +var uid = require('../internals/uid'); +var isCallable = require('../internals/is-callable'); +var isConstructor = require('../internals/is-constructor'); +var isObject = require('../internals/is-object'); +var isSymbol = require('../internals/is-symbol'); +var iterate = require('../internals/iterate'); +var anObject = require('../internals/an-object'); +var classof = require('../internals/classof'); +var hasOwn = require('../internals/has-own-property'); +var createProperty = require('../internals/create-property'); +var createNonEnumerableProperty = require('../internals/create-non-enumerable-property'); +var lengthOfArrayLike = require('../internals/length-of-array-like'); +var ERROR_STACK_INSTALLABLE = require('../internals/error-stack-installable'); + +var Object = global.Object; +var Date = global.Date; +var Error = global.Error; +var EvalError = global.EvalError; +var RangeError = global.RangeError; +var ReferenceError = global.ReferenceError; +var SyntaxError = global.SyntaxError; +var TypeError = global.TypeError; +var URIError = global.URIError; +var PerformanceMark = global.PerformanceMark; +var WebAssembly = global.WebAssembly; +var CompileError = WebAssembly && WebAssembly.CompileError || Error; +var LinkError = WebAssembly && WebAssembly.LinkError || Error; +var RuntimeError = WebAssembly && WebAssembly.RuntimeError || Error; +var DOMException = getBuiltin('DOMException'); +var Set = getBuiltin('Set'); +var Map = getBuiltin('Map'); +var MapPrototype = Map.prototype; +var mapHas = uncurryThis(MapPrototype.has); +var mapGet = uncurryThis(MapPrototype.get); +var mapSet = uncurryThis(MapPrototype.set); +var setAdd = uncurryThis(Set.prototype.add); +var push = uncurryThis([].push); +var bolleanValueOf = uncurryThis(true.valueOf); +var numberValueOf = uncurryThis(1.0.valueOf); +var stringValueOf = uncurryThis(''.valueOf); +var getTime = uncurryThis(Date.prototype.getTime); +var PERFORMANCE_MARK = uid('structuredClone'); +var DATA_CLONE_ERROR = 'DataCloneError'; +var TRANSFERRING = 'Transferring'; + +var checkBasicSemantic = function (structuredCloneImplementation) { + return !fails(function () { + var set1 = new global.Set([7]); + var set2 = structuredCloneImplementation(set1); + var number = structuredCloneImplementation(Object(7)); + return set2 == set1 || !set2.has(7) || typeof number != 'object' || number != 7; + }) && structuredCloneImplementation; +}; + +// https://github.com/whatwg/html/pull/5749 +var checkNewErrorsSemantic = function (structuredCloneImplementation) { + return !fails(function () { + var test = structuredCloneImplementation(new global.AggregateError([1], PERFORMANCE_MARK, { cause: 3 })); + return test.name != 'AggregateError' || test.errors[0] != 1 || test.message != PERFORMANCE_MARK || test.cause != 3; + }) && structuredCloneImplementation; +}; + +// FF94+, Safari TP134+, Chrome Canary 98+, NodeJS 17.0+, Deno 1.13+ +// current FF and Safari implementations can't clone errors +// https://bugzilla.mozilla.org/show_bug.cgi?id=1556604 +// no one of current implementations supports new (html/5749) error cloning semantic +var nativeStructuredClone = global.structuredClone; + +var FORCED_REPLACEMENT = IS_PURE || !checkNewErrorsSemantic(nativeStructuredClone); + +// Chrome 82+, Safari 14.1+, Deno 1.11+ +// Chrome 78-81 implementation swaps `.name` and `.message` of cloned `DOMException` +// Deno 1.2-1.10 implementations too naive +// NodeJS 16.0+ haven't `PerformanceMark` constructor, structured cloning implementation +// from `performance.mark` is too naive and can't clone, for example, `RegExp` or some boxed primitives +// https://github.com/nodejs/node/issues/40840 +// current Safari implementation can't clone errors +// no one of current implementations supports new (html/5749) error cloning semantic +var structuredCloneFromMark = !nativeStructuredClone && checkBasicSemantic(function (value) { + return new PerformanceMark(PERFORMANCE_MARK, { detail: value }).detail; +}); + +var nativeRestrictedStructuredClone = checkBasicSemantic(nativeStructuredClone) || structuredCloneFromMark; + +var throwUncloneable = function (type) { + throw new DOMException('Uncloneable type: ' + type, DATA_CLONE_ERROR); +}; + +var throwUnpolyfillable = function (type, kind) { + throw new DOMException((kind || 'Cloning') + ' of ' + type + ' cannot be properly polyfilled in this engine', DATA_CLONE_ERROR); +}; + +var structuredCloneInternal = function (value, map) { + if (isSymbol(value)) throwUncloneable('Symbol'); + if (!isObject(value)) return value; + // effectively preserves circular references + if (map) { + if (mapHas(map, value)) return mapGet(map, value); + } else map = new Map(); + + var type = classof(value); + var deep = false; + var C, name, cloned, dataTransfer, i, length, key; + + switch (type) { + case 'Array': + cloned = []; + deep = true; + break; + case 'Object': + cloned = {}; + deep = true; + break; + case 'Map': + cloned = new Map(); + deep = true; + break; + case 'Set': + cloned = new Set(); + deep = true; + break; + case 'Error': + name = value.name; + switch (name) { + case 'AggregateError': + cloned = getBuiltin('AggregateError')([]); + break; + case 'EvalError': + cloned = EvalError(); + break; + case 'RangeError': + cloned = RangeError(); + break; + case 'ReferenceError': + cloned = ReferenceError(); + break; + case 'SyntaxError': + cloned = SyntaxError(); + break; + case 'TypeError': + cloned = TypeError(); + break; + case 'URIError': + cloned = URIError(); + break; + case 'CompileError': + cloned = CompileError(); + break; + case 'LinkError': + cloned = LinkError(); + break; + case 'RuntimeError': + cloned = RuntimeError(); + break; + default: + cloned = Error(); + } + deep = true; + break; + case 'DOMException': + cloned = new DOMException(value.message, value.name); + deep = true; + break; + case 'DataView': + case 'Int8Array': + case 'Uint8Array': + case 'Uint8ClampedArray': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': + case 'BigInt64Array': + case 'BigUint64Array': + C = global[type]; + if (!isConstructor(C)) throwUnpolyfillable(type); + cloned = new C( + // this is safe, since arraybuffer cannot have circular references + structuredCloneInternal(value.buffer, map), + value.byteOffset, + type === 'DataView' ? value.byteLength : value.length + ); + break; + case 'DOMQuad': + C = global.DOMQuad; + if (isConstructor(C)) { + cloned = new C( + structuredCloneInternal(value.p1, map), + structuredCloneInternal(value.p2, map), + structuredCloneInternal(value.p3, map), + structuredCloneInternal(value.p4, map) + ); + } else if (nativeRestrictedStructuredClone) { + cloned = nativeRestrictedStructuredClone(value); + } else throwUnpolyfillable(type); + break; + case 'FileList': + C = global.DataTransfer; + if (isConstructor(C)) { + dataTransfer = new C(); + for (i = 0, length = lengthOfArrayLike(value); i < length; i++) { + dataTransfer.items.add(structuredCloneInternal(value[i], map)); + } + cloned = dataTransfer.files; + } else if (nativeRestrictedStructuredClone) { + cloned = nativeRestrictedStructuredClone(value); + } else throwUnpolyfillable(type); + break; + case 'ImageData': + C = global.ImageData; + if (isConstructor(C)) { + cloned = new C( + structuredCloneInternal(value.data, map), + value.width, + value.height, + { colorSpace: value.colorSpace } + ); + } else if (nativeRestrictedStructuredClone) { + cloned = nativeRestrictedStructuredClone(value); + } else throwUnpolyfillable(type); + break; + default: + if (nativeRestrictedStructuredClone) { + cloned = nativeRestrictedStructuredClone(value); + } else switch (type) { + case 'BigInt': + // can be a 3rd party polyfill + cloned = Object(value.valueOf()); + break; + case 'Boolean': + cloned = Object(bolleanValueOf(value)); + break; + case 'Number': + cloned = Object(numberValueOf(value)); + break; + case 'String': + cloned = Object(stringValueOf(value)); + break; + case 'Date': + cloned = new Date(getTime(value)); + break; + case 'RegExp': + cloned = new RegExp(value); + break; + case 'ArrayBuffer': + // detached buffers throws on `.slice` + try { + cloned = value.slice(0); + } catch (error) { + throw new DOMException('ArrayBuffer is deatched', DATA_CLONE_ERROR); + } break; + case 'SharedArrayBuffer': + // SharedArrayBuffer should use shared memory, we can't polyfill it, so return the original + cloned = value; + break; + case 'Blob': + try { + cloned = value.slice(0, value.size, value.type); + } catch (error) { + throwUnpolyfillable(type); + } break; + case 'DOMPoint': + case 'DOMPointReadOnly': + C = global[type]; + try { + cloned = C.fromPoint + ? C.fromPoint(value) + : new C(value.x, value.y, value.z, value.w); + } catch (error) { + throwUnpolyfillable(type); + } break; + case 'DOMRect': + case 'DOMRectReadOnly': + C = global[type]; + try { + cloned = C.fromRect + ? C.fromRect(value) + : new C(value.x, value.y, value.width, value.height); + } catch (error) { + throwUnpolyfillable(type); + } break; + case 'DOMMatrix': + case 'DOMMatrixReadOnly': + C = global[type]; + try { + cloned = C.fromMatrix + ? C.fromMatrix(value) + : new C(value); + } catch (error) { + throwUnpolyfillable(type); + } break; + case 'AudioData': + case 'VideoFrame': + if (!isCallable(value.clone)) throwUnpolyfillable(type); + try { + cloned = value.clone(); + } catch (error) { + throwUncloneable(type); + } break; + case 'File': + try { + cloned = new File([value], value.name, value); + } catch (error) { + throwUnpolyfillable(type); + } break; + case 'CryptoKey': + case 'GPUCompilationMessage': + case 'GPUCompilationInfo': + case 'ImageBitmap': + case 'RTCCertificate': + case 'WebAssembly.Module': + throwUnpolyfillable(type); + // break omitted + default: + throwUncloneable(type); + } + } + + mapSet(map, value, cloned); + + if (deep) switch (type) { + case 'Array': + case 'Object': + for (key in value) if (hasOwn(value, key)) { + createProperty(cloned, key, structuredCloneInternal(value[key], map)); + } break; + case 'Map': + value.forEach(function (v, k) { + mapSet(cloned, structuredCloneInternal(k, map), structuredCloneInternal(v, map)); + }); + break; + case 'Set': + value.forEach(function (v) { + setAdd(cloned, structuredCloneInternal(v, map)); + }); + break; + case 'Error': + createNonEnumerableProperty(cloned, 'message', structuredCloneInternal(value.message, map)); + if (hasOwn(value, 'cause')) { + createNonEnumerableProperty(cloned, 'cause', structuredCloneInternal(value.cause, map)); + } + if (name == 'AggregateError') { + cloned.errors = structuredCloneInternal(value.errors, map); + } // break omitted + case 'DOMException': + if (ERROR_STACK_INSTALLABLE) { + createNonEnumerableProperty(cloned, 'stack', structuredCloneInternal(value.stack, map)); + } + } + + return cloned; +}; + +var PROPER_TRANSFER = nativeStructuredClone && !fails(function () { + var buffer = new global.ArrayBuffer(8); + var clone = nativeStructuredClone(buffer, { transfer: [buffer] }); + return buffer.byteLength != 0 || clone.byteLength != 8; +}); + +var tryToTransfer = function (rawTransfer, map) { + if (!isObject(rawTransfer)) throw TypeError('Transfer option cannot be converted to a sequence'); + + var transfer = []; + + iterate(rawTransfer, function (value) { + push(transfer, anObject(value)); + }); + + var i = 0; + var length = lengthOfArrayLike(transfer); + var value, type, C, transferredArray, transferred, canvas, context; + + if (PROPER_TRANSFER) { + transferredArray = nativeStructuredClone(transfer, { transfer: transfer }); + while (i < length) mapSet(map, transfer[i], transferredArray[i++]); + } else while (i < length) { + value = transfer[i++]; + if (mapHas(map, value)) throw new DOMException('Duplicate transferable', DATA_CLONE_ERROR); + + type = classof(value); + + switch (type) { + case 'ImageBitmap': + C = global.OffscreenCanvas; + if (!isConstructor(C)) throwUnpolyfillable(type, TRANSFERRING); + try { + canvas = new C(value.width, value.height); + context = canvas.getContext('bitmaprenderer'); + context.transferFromImageBitmap(value); + transferred = canvas.transferToImageBitmap(); + } catch (error) { /* empty */ } + break; + case 'AudioData': + case 'VideoFrame': + if (!isCallable(value.clone) || !isCallable(value.close)) throwUnpolyfillable(type, TRANSFERRING); + try { + transferred = value.clone(); + value.close(); + } catch (error) { /* empty */ } + break; + case 'ArrayBuffer': + case 'MessagePort': + case 'OffscreenCanvas': + case 'ReadableStream': + case 'TransformStream': + case 'WritableStream': + throwUnpolyfillable(type, TRANSFERRING); + } + + if (transferred === undefined) throw new DOMException('This object cannot be transferred: ' + type, DATA_CLONE_ERROR); + mapSet(map, value, transferred); + } +}; + +$({ global: true, enumerable: true, sham: !PROPER_TRANSFER, forced: FORCED_REPLACEMENT }, { + structuredClone: function structuredClone(value /* , { transfer } */) { + var options = arguments.length > 1 ? anObject(arguments[1]) : undefined; + var transfer = options ? options.transfer : undefined; + var map; + + if (transfer !== undefined) { + map = new Map(); + tryToTransfer(transfer, map); + } + + return structuredCloneInternal(value, map); + } +}); diff --git a/packages/core-js/stable/structured-clone.js b/packages/core-js/stable/structured-clone.js new file mode 100644 index 000000000000..36e2b60d2f3a --- /dev/null +++ b/packages/core-js/stable/structured-clone.js @@ -0,0 +1,12 @@ +require('../modules/es.error.to-string'); +require('../modules/es.array.iterator'); +require('../modules/es.object.to-string'); +require('../modules/es.map'); +require('../modules/es.set'); +require('../modules/web.dom-exception.constructor'); +require('../modules/web.dom-exception.stack'); +require('../modules/web.dom-exception.to-string-tag'); +require('../modules/web.structured-clone'); +var path = require('../internals/path'); + +module.exports = path.structuredClone; diff --git a/packages/core-js/web/index.js b/packages/core-js/web/index.js index 79a4ec54ee46..69bf1f429251 100644 --- a/packages/core-js/web/index.js +++ b/packages/core-js/web/index.js @@ -5,6 +5,7 @@ require('../modules/web.dom-exception.stack'); require('../modules/web.dom-exception.to-string-tag'); require('../modules/web.immediate'); require('../modules/web.queue-microtask'); +require('../modules/web.structured-clone'); require('../modules/web.timers'); require('../modules/web.url'); require('../modules/web.url.to-json'); diff --git a/packages/core-js/web/structured-clone.js b/packages/core-js/web/structured-clone.js new file mode 100644 index 000000000000..283f3aed5f08 --- /dev/null +++ b/packages/core-js/web/structured-clone.js @@ -0,0 +1,8 @@ +require('../modules/es.array.iterator'); +require('../modules/es.object.to-string'); +require('../modules/es.map'); +require('../modules/es.set'); +require('../modules/web.structured-clone'); +var path = require('../internals/path'); + +module.exports = path.structuredClone; diff --git a/tests/commonjs.mjs b/tests/commonjs.mjs index c749fb8b83e5..008a03076aa6 100644 --- a/tests/commonjs.mjs +++ b/tests/commonjs.mjs @@ -554,6 +554,7 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(typeof load(NS, 'set-timeout') == 'function'); ok(typeof load(NS, 'set-interval') == 'function'); ok(typeof load(NS, 'set-immediate') == 'function'); + ok(load(NS, 'structured-clone')(42) === 42); ok(typeof load(NS, 'clear-immediate') == 'function'); ok(typeof load(NS, 'queue-microtask') == 'function'); ok(typeof load(NS, 'url') == 'function'); @@ -847,6 +848,7 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(load('web/dom-collections')); ok(load('web/immediate')); ok(load('web/queue-microtask')); + ok(load('web/structured-clone')(42) === 42); ok(load('web/timers')); ok(load('web/url')); ok(load('web/url-search-params')); diff --git a/tests/compat/tests.js b/tests/compat/tests.js index 22ea4670af90..c021dbba6291 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -1671,6 +1671,10 @@ GLOBAL.tests = { 'web.queue-microtask': function () { return Object.getOwnPropertyDescriptor(GLOBAL, 'queueMicrotask').value; }, + 'web.structured-clone': function () { + var test = structuredClone(new AggregateError([1], 'a', { cause: 3 })); + return test.name == 'AggregateError' && test.errors[0] == 1 && test.message == 'a' && test.cause == 3; + }, 'web.timers': function () { return !/MSIE .\./.test(USERAGENT); }, diff --git a/tests/pure/web.structured-clone.js b/tests/pure/web.structured-clone.js new file mode 100644 index 000000000000..63dd0c8c1cd1 --- /dev/null +++ b/tests/pure/web.structured-clone.js @@ -0,0 +1,425 @@ +// Originally from: https://github.com/web-platform-tests/wpt/blob/4b35e758e2fc4225368304b02bcec9133965fd1a/IndexedDB/structured-clone.any.js +// Copyright © web-platform-tests contributors. Available under the 3-Clause BSD License. +/* eslint-disable es/no-typed-arrays -- safe */ +import { GLOBAL, NODE } from '../helpers/constants'; +import { fromSource } from '../helpers/helpers'; + +import structuredClone from 'core-js-pure/stable/structured-clone'; +import from from 'core-js-pure/es/array/from'; +import assign from 'core-js-pure/es/object/assign'; +import getPrototypeOf from 'core-js-pure/es/object/get-prototype-of'; +import keys from 'core-js-pure/es/object/keys'; +import Symbol from 'core-js-pure/es/symbol'; +import Map from 'core-js-pure/es/map'; +import Set from 'core-js-pure/es/set'; +import AggregateError from 'core-js-pure/es/aggregate-error'; +import DOMException from 'core-js-pure/stable/dom-exception'; + +QUnit.module('structuredClone', () => { + QUnit.test('identity', assert => { + assert.isFunction(structuredClone, 'structuredClone is a function'); + assert.name(structuredClone, 'structuredClone'); + assert.arity(structuredClone, 1); + }); + + function cloneTest(value, verifyFunc) { + verifyFunc(value, structuredClone(value)); + } + + // Specialization of cloneTest() for objects, with common asserts. + function cloneObjectTest(assert, value, verifyFunc) { + cloneTest(value, (orig, clone) => { + assert.notSame(orig, clone, 'clone should have different reference'); + assert.same(typeof clone, 'object', 'clone should be an object'); + // https://github.com/qunitjs/node-qunit/issues/146 + assert.true(getPrototypeOf(orig) === getPrototypeOf(clone), 'clone should have same prototype'); + verifyFunc(orig, clone); + }); + } + + // ECMAScript types + + // Primitive values: Undefined, Null, Boolean, Number, BigInt, String + const booleans = [false, true]; + const numbers = [ + NaN, + -Infinity, + -Number.MAX_VALUE, + -0xFFFFFFFF, + -0x80000000, + -0x7FFFFFFF, + -1, + -Number.MIN_VALUE, + -0, + 0, + 1, + Number.MIN_VALUE, + 0x7FFFFFFF, + 0x80000000, + 0xFFFFFFFF, + Number.MAX_VALUE, + Infinity, + ]; + + const bigints = fromSource(`[ + -12345678901234567890n, + -1n, + 0n, + 1n, + 12345678901234567890n, + ]`) || []; + + const strings = [ + '', + 'this is a sample string', + 'null(\0)', + ]; + + QUnit.test('primitives', assert => { + const primitives = [undefined, null].concat(booleans, numbers, bigints, strings); + + for (const value of primitives) cloneTest(value, (orig, clone) => { + assert.same(orig, clone, 'primitives should be same after cloned'); + }); + }); + + // "Primitive" Objects (Boolean, Number, BigInt, String) + QUnit.test('primitive objects', assert => { + const primitives = [].concat(booleans, numbers, bigints, strings); + + for (const value of primitives) cloneObjectTest(assert, Object(value), (orig, clone) => { + assert.same(orig.valueOf(), clone.valueOf(), 'primitive wrappers should have same value'); + }); + }); + + // Dates + QUnit.test('Date', assert => { + const dates = [ + new Date(-1e13), + new Date(-1e12), + new Date(-1e9), + new Date(-1e6), + new Date(-1e3), + new Date(0), + new Date(1e3), + new Date(1e6), + new Date(1e9), + new Date(1e12), + new Date(1e13), + ]; + + for (const date of dates) cloneTest(date, (orig, clone) => { + assert.notSame(orig, clone); + assert.same(typeof clone, 'object'); + assert.same(getPrototypeOf(orig), getPrototypeOf(clone)); + assert.same(orig.valueOf(), clone.valueOf()); + }); + }); + + // Regular Expressions + QUnit.test('RegExp', assert => { + const regexes = [ + new RegExp(), + /abc/, + /abc/g, + /abc/i, + /abc/gi, + /abc/, + /abc/g, + /abc/i, + /abc/gi, + ]; + + const giuy = fromSource('/abc/giuy'); + if (giuy) regexes.push(giuy); + + for (const regex of regexes) cloneObjectTest(assert, regex, (orig, clone) => { + assert.same(orig.toString(), clone.toString(), `regex ${ regex }`); + }); + }); + + // ArrayBuffer + if (typeof Uint8Array == 'function') QUnit.test('ArrayBuffer', assert => { // Crashes + cloneObjectTest(assert, new Uint8Array([0, 1, 254, 255]).buffer, (orig, clone) => { + assert.arrayEqual(new Uint8Array(orig), new Uint8Array(clone)); + }); + }); + + // TODO SharedArrayBuffer + + // Array Buffer Views + if (typeof Uint8ClampedArray == 'function') QUnit.test('ArrayBufferView', assert => { + const arrays = [ + new Uint8Array([]), + new Uint8Array([0, 1, 254, 255]), + new Uint16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), + new Uint32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), + new Int8Array([0, 1, 254, 255]), + new Int16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), + new Int32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), + new Uint8ClampedArray([0, 1, 254, 255]), + new Float32Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN]), + new Float64Array([-Infinity, -Number.MAX_VALUE, -Number.MIN_VALUE, 0, Number.MIN_VALUE, Number.MAX_VALUE, Infinity, NaN]), + ]; + + for (const array of arrays) cloneObjectTest(assert, array, (orig, clone) => { + assert.arrayEqual(orig, clone); + }); + }); + + // Map + QUnit.test('Map', assert => { + cloneObjectTest(assert, new Map([[1, 2], [3, 4]]), (orig, clone) => { + assert.deepEqual(from(orig.keys()), from(clone.keys())); + assert.deepEqual(from(orig.values()), from(clone.values())); + }); + }); + + // Set + QUnit.test('Set', assert => { + cloneObjectTest(assert, new Set([1, 2, 3, 4]), (orig, clone) => { + assert.deepEqual(from(orig.values()), from(clone.values())); + }); + }); + + // Error + QUnit.test('Error', assert => { + const errors = [ + ['Error', new Error()], + ['Error', new Error('abc', 'def', { cause: 42 })], + ['EvalError', new EvalError()], + ['EvalError', new EvalError('ghi', 'jkl', { cause: 42 })], + ['RangeError', new RangeError()], + ['RangeError', new RangeError('ghi', 'jkl', { cause: 42 })], + ['ReferenceError', new ReferenceError()], + ['ReferenceError', new ReferenceError('ghi', 'jkl', { cause: 42 })], + ['SyntaxError', new SyntaxError()], + ['SyntaxError', new SyntaxError('ghi', 'jkl', { cause: 42 })], + ['TypeError', new TypeError()], + ['TypeError', new TypeError('ghi', 'jkl', { cause: 42 })], + ['URIError', new URIError()], + ['URIError', new URIError('ghi', 'jkl', { cause: 42 })], + ['AggregateError', new AggregateError([1, 2])], + ['AggregateError', new AggregateError([1, 2], 42, { cause: 42 })], + ]; + + const compile = fromSource('WebAssembly.CompileError()'); + const link = fromSource('WebAssembly.LinkError()'); + const runtime = fromSource('WebAssembly.RuntimeError()'); + + if (compile) errors.push(['CompileError', compile]); + if (link) errors.push(['LinkError', link]); + if (runtime) errors.push(['RuntimeError', runtime]); + + for (const [name, error] of errors) cloneObjectTest(assert, error, (orig, clone) => { + assert.same(orig.constructor, clone.constructor, `${ name }#constructor`); + assert.same(orig.name, clone.name, `${ name }#name`); + assert.same(orig.message, clone.message, `${ name }#message`); + assert.same(orig.stack, clone.stack, `${ name }#stack`); + assert.same(orig.cause, clone.cause, `${ name }#cause`); + assert.deepEqual(orig.errors, clone.errors, `${ name }#errors`); + }); + }); + + // Arrays + QUnit.test('Array', assert => { + const arrays = [ + [], + [1, 2, 3], + assign( + ['foo', 'bar'], + { 10: true, 11: false, 20: 123, 21: 456, 30: null }), + assign( + ['foo', 'bar'], + { a: true, b: false, foo: 123, bar: 456, '': null }), + ]; + + for (const array of arrays) cloneObjectTest(assert, array, (orig, clone) => { + assert.deepEqual(orig, clone, `array content should be same: ${ array }`); + assert.deepEqual(keys(orig), keys(clone), `array key should be same: ${ array }`); + for (const key of keys(orig)) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + + // Objects + QUnit.test('Object', assert => { + cloneObjectTest(assert, { foo: true, bar: false }, (orig, clone) => { + assert.deepEqual(keys(orig), keys(clone)); + for (const key of keys(orig)) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + + // [Serializable] Platform objects + + // Geometry types + if (typeof DOMMatrix == 'function') { + QUnit.test('Geometry types, DOMMatrix', assert => { + cloneObjectTest(assert, new DOMMatrix(), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMMatrixReadOnly == 'function' && typeof DOMMatrixReadOnly.fromMatrix == 'function') { + QUnit.test('Geometry types, DOMMatrixReadOnly', assert => { + cloneObjectTest(assert, new DOMMatrixReadOnly(), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMPoint == 'function') { + QUnit.test('Geometry types, DOMPoint', assert => { + cloneObjectTest(assert, new DOMPoint(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMPointReadOnly == 'function' && typeof DOMPointReadOnly.fromPoint == 'function') { + QUnit.test('Geometry types, DOMPointReadOnly', assert => { + cloneObjectTest(assert, new DOMPointReadOnly(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMQuad == 'function' && typeof DOMPoint == 'function') { + QUnit.test('Geometry types, DOMQuad', assert => { + cloneObjectTest(assert, new DOMQuad( + new DOMPoint(1, 2, 3, 4), + new DOMPoint(2, 2, 3, 4), + new DOMPoint(1, 3, 3, 4), + new DOMPoint(1, 2, 4, 4), + ), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.deepEqual(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (fromSource('new DOMRect(1, 2, 3, 4)')) { + QUnit.test('Geometry types, DOMRect', assert => { + cloneObjectTest(assert, new DOMRect(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMRectReadOnly == 'function' && typeof DOMRectReadOnly.fromRect == 'function') { + QUnit.test('Geometry types, DOMRectReadOnly', assert => { + cloneObjectTest(assert, new DOMRectReadOnly(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (fromSource('new ImageData(8, 8)')) QUnit.test('ImageData', assert => { + const imageData = new ImageData(8, 8); + for (let i = 0; i < 256; ++i) { + imageData.data[i] = i; + } + cloneObjectTest(assert, imageData, (orig, clone) => { + assert.same(orig.width, clone.width); + assert.same(orig.height, clone.height); + assert.same(orig.colorSpace, clone.colorSpace); + assert.arrayEqual(orig.data, clone.data); + }); + }); + + if (fromSource('new Blob(["test"])')) QUnit.test('Blob', assert => { + cloneObjectTest( + assert, + new Blob(['This is a test.'], { type: 'a/b' }), + (orig, clone) => { + assert.same(orig.size, clone.size); + assert.same(orig.type, clone.type); + // TODO: async + // assert.same(await orig.text(), await clone.text()); + }); + }); + + QUnit.test('DOMException', assert => { + const errors = [ + new DOMException(), + new DOMException('foo', 'DataCloneError'), + ]; + + for (const error of errors) cloneObjectTest(assert, error, (orig, clone) => { + assert.same(orig.name, clone.name); + assert.same(orig.message, clone.message); + assert.same(orig.code, clone.code); + assert.same(orig.stack, clone.stack); + }); + }); + + if (fromSource('new File(["test"], "foo.txt")')) QUnit.test('File', assert => { + cloneObjectTest( + assert, + new File(['This is a test.'], 'foo.txt', { type: 'c/d' }), + (orig, clone) => { + assert.same(orig.size, clone.size); + assert.same(orig.type, clone.type); + assert.same(orig.name, clone.name); + assert.same(orig.lastModified, clone.lastModified); + // TODO: async + // assert.same(await orig.text(), await clone.text()); + }); + }); + + // FileList + if (fromSource('new File(["test"], "foo.txt")') && fromSource('new DataTransfer()')) QUnit.test('FileList', assert => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['test'], 'foo.txt')); + cloneObjectTest( + assert, + transfer.files, + (orig, clone) => { + assert.same(1, clone.length); + assert.same(orig[0].size, clone[0].size); + assert.same(orig[0].type, clone[0].type); + assert.same(orig[0].name, clone[0].name); + assert.same(orig[0].lastModified, clone[0].lastModified); + }, + ); + }); + + // Non-serializable types + QUnit.test('Non-serializable types', assert => { + const nons = [ + function () { return 1; }, + Symbol('desc'), + GLOBAL, + ]; + + const event = fromSource('new Event("")'); + const port = fromSource('new MessageChannel().port1'); + + // NodeJS events are simple objects + if (event && !NODE) nons.push(event); + if (port) nons.push(port); + + for (const it of nons) { + // native NodeJS `structuredClone` throws a `TypeError` on transferable non-serializable instead of `DOMException` + // https://github.com/nodejs/node/issues/40841 + assert.throws(() => structuredClone(it)); + } + }); +}); diff --git a/tests/tests/web.structured-clone.js b/tests/tests/web.structured-clone.js new file mode 100644 index 000000000000..c69816949688 --- /dev/null +++ b/tests/tests/web.structured-clone.js @@ -0,0 +1,417 @@ +// Originally from: https://github.com/web-platform-tests/wpt/blob/4b35e758e2fc4225368304b02bcec9133965fd1a/IndexedDB/structured-clone.any.js +// Copyright © web-platform-tests contributors. Available under the 3-Clause BSD License. +import { GLOBAL, NODE } from '../helpers/constants'; +import { fromSource } from '../helpers/helpers'; + +const { from } = Array; +const { assign, getPrototypeOf, keys } = Object; + +QUnit.module('structuredClone', () => { + QUnit.test('identity', assert => { + assert.isFunction(structuredClone, 'structuredClone is a function'); + assert.name(structuredClone, 'structuredClone'); + assert.arity(structuredClone, 1); + if (!NODE) assert.looksNative(structuredClone); + }); + + function cloneTest(value, verifyFunc) { + verifyFunc(value, structuredClone(value)); + } + + // Specialization of cloneTest() for objects, with common asserts. + function cloneObjectTest(assert, value, verifyFunc) { + cloneTest(value, (orig, clone) => { + assert.notSame(orig, clone, 'clone should have different reference'); + assert.same(typeof clone, 'object', 'clone should be an object'); + // https://github.com/qunitjs/node-qunit/issues/146 + assert.true(getPrototypeOf(orig) === getPrototypeOf(clone), 'clone should have same prototype'); + verifyFunc(orig, clone); + }); + } + + // ECMAScript types + + // Primitive values: Undefined, Null, Boolean, Number, BigInt, String + const booleans = [false, true]; + const numbers = [ + NaN, + -Infinity, + -Number.MAX_VALUE, + -0xFFFFFFFF, + -0x80000000, + -0x7FFFFFFF, + -1, + -Number.MIN_VALUE, + -0, + 0, + 1, + Number.MIN_VALUE, + 0x7FFFFFFF, + 0x80000000, + 0xFFFFFFFF, + Number.MAX_VALUE, + Infinity, + ]; + + const bigints = fromSource(`[ + -12345678901234567890n, + -1n, + 0n, + 1n, + 12345678901234567890n, + ]`) || []; + + const strings = [ + '', + 'this is a sample string', + 'null(\0)', + ]; + + QUnit.test('primitives', assert => { + const primitives = [undefined, null].concat(booleans, numbers, bigints, strings); + + for (const value of primitives) cloneTest(value, (orig, clone) => { + assert.same(orig, clone, 'primitives should be same after cloned'); + }); + }); + + // "Primitive" Objects (Boolean, Number, BigInt, String) + QUnit.test('primitive objects', assert => { + const primitives = [].concat(booleans, numbers, bigints, strings); + + for (const value of primitives) cloneObjectTest(assert, Object(value), (orig, clone) => { + assert.same(orig.valueOf(), clone.valueOf(), 'primitive wrappers should have same value'); + }); + }); + + // Dates + QUnit.test('Date', assert => { + const dates = [ + new Date(-1e13), + new Date(-1e12), + new Date(-1e9), + new Date(-1e6), + new Date(-1e3), + new Date(0), + new Date(1e3), + new Date(1e6), + new Date(1e9), + new Date(1e12), + new Date(1e13), + ]; + + for (const date of dates) cloneTest(date, (orig, clone) => { + assert.notSame(orig, clone); + assert.same(typeof clone, 'object'); + assert.same(getPrototypeOf(orig), getPrototypeOf(clone)); + assert.same(orig.valueOf(), clone.valueOf()); + }); + }); + + // Regular Expressions + QUnit.test('RegExp', assert => { + const regexes = [ + new RegExp(), + /abc/, + /abc/g, + /abc/i, + /abc/gi, + /abc/, + /abc/g, + /abc/i, + /abc/gi, + ]; + + const giuy = fromSource('/abc/giuy'); + if (giuy) regexes.push(giuy); + + for (const regex of regexes) cloneObjectTest(assert, regex, (orig, clone) => { + assert.same(orig.toString(), clone.toString(), `regex ${ regex }`); + }); + }); + + // ArrayBuffer + if (typeof Uint8Array == 'function') QUnit.test('ArrayBuffer', assert => { // Crashes + cloneObjectTest(assert, new Uint8Array([0, 1, 254, 255]).buffer, (orig, clone) => { + assert.arrayEqual(new Uint8Array(orig), new Uint8Array(clone)); + }); + }); + + // TODO SharedArrayBuffer + + // Array Buffer Views + if (typeof Uint8ClampedArray == 'function') QUnit.test('ArrayBufferView', assert => { + const arrays = [ + new Uint8Array([]), + new Uint8Array([0, 1, 254, 255]), + new Uint16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), + new Uint32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), + new Int8Array([0, 1, 254, 255]), + new Int16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), + new Int32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), + new Uint8ClampedArray([0, 1, 254, 255]), + new Float32Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN]), + new Float64Array([-Infinity, -Number.MAX_VALUE, -Number.MIN_VALUE, 0, Number.MIN_VALUE, Number.MAX_VALUE, Infinity, NaN]), + ]; + + for (const array of arrays) cloneObjectTest(assert, array, (orig, clone) => { + assert.arrayEqual(orig, clone); + }); + }); + + // Map + QUnit.test('Map', assert => { + cloneObjectTest(assert, new Map([[1, 2], [3, 4]]), (orig, clone) => { + assert.deepEqual(from(orig.keys()), from(clone.keys())); + assert.deepEqual(from(orig.values()), from(clone.values())); + }); + }); + + // Set + QUnit.test('Set', assert => { + cloneObjectTest(assert, new Set([1, 2, 3, 4]), (orig, clone) => { + assert.deepEqual(from(orig.values()), from(clone.values())); + }); + }); + + // Error + QUnit.test('Error', assert => { + const errors = [ + ['Error', new Error()], + ['Error', new Error('abc', 'def', { cause: 42 })], + ['EvalError', new EvalError()], + ['EvalError', new EvalError('ghi', 'jkl', { cause: 42 })], + ['RangeError', new RangeError()], + ['RangeError', new RangeError('ghi', 'jkl', { cause: 42 })], + ['ReferenceError', new ReferenceError()], + ['ReferenceError', new ReferenceError('ghi', 'jkl', { cause: 42 })], + ['SyntaxError', new SyntaxError()], + ['SyntaxError', new SyntaxError('ghi', 'jkl', { cause: 42 })], + ['TypeError', new TypeError()], + ['TypeError', new TypeError('ghi', 'jkl', { cause: 42 })], + ['URIError', new URIError()], + ['URIError', new URIError('ghi', 'jkl', { cause: 42 })], + ['AggregateError', new AggregateError([1, 2])], + ['AggregateError', new AggregateError([1, 2], 42, { cause: 42 })], + ]; + + const compile = fromSource('WebAssembly.CompileError()'); + const link = fromSource('WebAssembly.LinkError()'); + const runtime = fromSource('WebAssembly.RuntimeError()'); + + if (compile) errors.push(['CompileError', compile]); + if (link) errors.push(['LinkError', link]); + if (runtime) errors.push(['RuntimeError', runtime]); + + for (const [name, error] of errors) cloneObjectTest(assert, error, (orig, clone) => { + assert.same(orig.constructor, clone.constructor, `${ name }#constructor`); + assert.same(orig.name, clone.name, `${ name }#name`); + assert.same(orig.message, clone.message, `${ name }#message`); + assert.same(orig.stack, clone.stack, `${ name }#stack`); + assert.same(orig.cause, clone.cause, `${ name }#cause`); + assert.deepEqual(orig.errors, clone.errors, `${ name }#errors`); + }); + }); + + // Arrays + QUnit.test('Array', assert => { + const arrays = [ + [], + [1, 2, 3], + assign( + ['foo', 'bar'], + { 10: true, 11: false, 20: 123, 21: 456, 30: null }), + assign( + ['foo', 'bar'], + { a: true, b: false, foo: 123, bar: 456, '': null }), + ]; + + for (const array of arrays) cloneObjectTest(assert, array, (orig, clone) => { + assert.deepEqual(orig, clone, `array content should be same: ${ array }`); + assert.deepEqual(keys(orig), keys(clone), `array key should be same: ${ array }`); + for (const key of keys(orig)) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + + // Objects + QUnit.test('Object', assert => { + cloneObjectTest(assert, { foo: true, bar: false }, (orig, clone) => { + assert.deepEqual(keys(orig), keys(clone)); + for (const key of keys(orig)) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + + // [Serializable] Platform objects + + // Geometry types + if (typeof DOMMatrix == 'function') { + QUnit.test('Geometry types, DOMMatrix', assert => { + cloneObjectTest(assert, new DOMMatrix(), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMMatrixReadOnly == 'function' && typeof DOMMatrixReadOnly.fromMatrix == 'function') { + QUnit.test('Geometry types, DOMMatrixReadOnly', assert => { + cloneObjectTest(assert, new DOMMatrixReadOnly(), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMPoint == 'function') { + QUnit.test('Geometry types, DOMPoint', assert => { + cloneObjectTest(assert, new DOMPoint(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMPointReadOnly == 'function' && typeof DOMPointReadOnly.fromPoint == 'function') { + QUnit.test('Geometry types, DOMPointReadOnly', assert => { + cloneObjectTest(assert, new DOMPointReadOnly(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMQuad == 'function' && typeof DOMPoint == 'function') { + QUnit.test('Geometry types, DOMQuad', assert => { + cloneObjectTest(assert, new DOMQuad( + new DOMPoint(1, 2, 3, 4), + new DOMPoint(2, 2, 3, 4), + new DOMPoint(1, 3, 3, 4), + new DOMPoint(1, 2, 4, 4), + ), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.deepEqual(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (fromSource('new DOMRect(1, 2, 3, 4)')) { + QUnit.test('Geometry types, DOMRect', assert => { + cloneObjectTest(assert, new DOMRect(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (typeof DOMRectReadOnly == 'function' && typeof DOMRectReadOnly.fromRect == 'function') { + QUnit.test('Geometry types, DOMRectReadOnly', assert => { + cloneObjectTest(assert, new DOMRectReadOnly(1, 2, 3, 4), (orig, clone) => { + for (const key of keys(getPrototypeOf(orig))) { + assert.same(orig[key], clone[key], `Property ${ key }`); + } + }); + }); + } + + if (fromSource('new ImageData(8, 8)')) QUnit.test('ImageData', assert => { + const imageData = new ImageData(8, 8); + for (let i = 0; i < 256; ++i) { + imageData.data[i] = i; + } + cloneObjectTest(assert, imageData, (orig, clone) => { + assert.same(orig.width, clone.width); + assert.same(orig.height, clone.height); + assert.same(orig.colorSpace, clone.colorSpace); + assert.arrayEqual(orig.data, clone.data); + }); + }); + + if (fromSource('new Blob(["test"])')) QUnit.test('Blob', assert => { + cloneObjectTest( + assert, + new Blob(['This is a test.'], { type: 'a/b' }), + (orig, clone) => { + assert.same(orig.size, clone.size); + assert.same(orig.type, clone.type); + // TODO: async + // assert.same(await orig.text(), await clone.text()); + }); + }); + + QUnit.test('DOMException', assert => { + const errors = [ + new DOMException(), + new DOMException('foo', 'DataCloneError'), + ]; + + for (const error of errors) cloneObjectTest(assert, error, (orig, clone) => { + assert.same(orig.name, clone.name); + assert.same(orig.message, clone.message); + assert.same(orig.code, clone.code); + assert.same(orig.stack, clone.stack); + }); + }); + + if (fromSource('new File(["test"], "foo.txt")')) QUnit.test('File', assert => { + cloneObjectTest( + assert, + new File(['This is a test.'], 'foo.txt', { type: 'c/d' }), + (orig, clone) => { + assert.same(orig.size, clone.size); + assert.same(orig.type, clone.type); + assert.same(orig.name, clone.name); + assert.same(orig.lastModified, clone.lastModified); + // TODO: async + // assert.same(await orig.text(), await clone.text()); + }); + }); + + // FileList + if (fromSource('new File(["test"], "foo.txt")') && fromSource('new DataTransfer()')) QUnit.test('FileList', assert => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['test'], 'foo.txt')); + cloneObjectTest( + assert, + transfer.files, + (orig, clone) => { + assert.same(1, clone.length); + assert.same(orig[0].size, clone[0].size); + assert.same(orig[0].type, clone[0].type); + assert.same(orig[0].name, clone[0].name); + assert.same(orig[0].lastModified, clone[0].lastModified); + }, + ); + }); + + // Non-serializable types + QUnit.test('Non-serializable types', assert => { + const nons = [ + function () { return 1; }, + Symbol('desc'), + GLOBAL, + ]; + + const event = fromSource('new Event("")'); + const port = fromSource('new MessageChannel().port1'); + + // NodeJS events are simple objects + if (event && !NODE) nons.push(event); + if (port) nons.push(port); + + for (const it of nons) { + // native NodeJS `structuredClone` throws a `TypeError` on transferable non-serializable instead of `DOMException` + // https://github.com/nodejs/node/issues/40841 + assert.throws(() => structuredClone(it)); + } + }); +});