diff --git a/python/pythonmonkey/__init__.py b/python/pythonmonkey/__init__.py index 957713ff..8f09242f 100644 --- a/python/pythonmonkey/__init__.py +++ b/python/pythonmonkey/__init__.py @@ -14,4 +14,5 @@ require("base64") require("timers") require("url") +require("structured-clone") require("XMLHttpRequest") diff --git a/python/pythonmonkey/builtin_modules/structured-clone.js b/python/pythonmonkey/builtin_modules/structured-clone.js new file mode 100644 index 00000000..2188feb2 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/structured-clone.js @@ -0,0 +1,15 @@ +/** + * @file structured-clone.js + * Polyfill the structuredClone() global + * + * @author Dan Desjardins + * @date November 2026 + * + * @copyright Copyright (c) 2026 Distributive Corp. + */ + +// Apply polyfill from core-js +require('./dom-exception'); +require('core-js/actual/structured-clone'); + +exports.structuredClone = globalThis.structuredClone; diff --git a/python/pythonmonkey/global.d.ts b/python/pythonmonkey/global.d.ts index 6c350891..c42299b1 100644 --- a/python/pythonmonkey/global.d.ts +++ b/python/pythonmonkey/global.d.ts @@ -64,6 +64,9 @@ declare var clearInterval: typeof import("timers").clearInterval; declare var URL: typeof import("url").URL; declare var URLSearchParams: typeof import("url").URLSearchParams; +// Expose `structuredClone` +declare function structuredClone(value: T, options?: { transfer?: any[] }): T; + // Expose `XMLHttpRequest` (XHR) API declare var XMLHttpRequest: typeof import("XMLHttpRequest").XMLHttpRequest; diff --git a/tests/js/structured-clone.simple b/tests/js/structured-clone.simple new file mode 100644 index 00000000..5d36355a --- /dev/null +++ b/tests/js/structured-clone.simple @@ -0,0 +1,28 @@ +/** + * @file structured-clone.simple + * Simple test for the structuredClone() global + * @author Dan Desjardins + * @date November 2026 + */ + +function assert(cond, message) +{ + if (!cond) throw new Error(message); +} + +assert(typeof structuredClone === 'function', 'structuredClone is not a global function'); + +const source = { list: ['a'], nested: { map: new Map([[1, { x: 2 }]]), date: new Date(0), bytes: new Uint8Array([1, 2, 3]) } }; +source.self = source; +const copy = structuredClone(source); + +copy.list.push('b'); +assert(source.list.length === 1, 'clone shares an array with its source'); +assert(copy.nested.map.get(1).x === 2, 'Map entries were not cloned'); +assert(copy.nested.date instanceof Date && copy.nested.date.getTime() === 0, 'Date was not cloned'); +assert(copy.nested.bytes.join() === '1,2,3' && copy.nested.bytes !== source.nested.bytes, 'Uint8Array was not cloned'); +assert(copy.self === copy, 'cycle was not preserved'); + +let error; +try { structuredClone(function f() {}); } catch (e) { error = e; } +assert(error && error.name === 'DataCloneError', 'cloning a function did not throw DataCloneError');