All files / src/internal/shared clone.js

94.73% Statements 108/114
85.71% Branches 30/35
100% Functions 2/2
94.59% Lines 105/111

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 1122x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x 73x 73x 73x 1x 1x 73x 2x 2x 2x 2x 2x 2x 2x 2x 2x 73x 73x 73x       2x 2x 2x 2x 2x 2x 2x 2x 2x 329x 329x 140x 140x 135x 140x 83x 83x 83x 83x 195x 195x 83x 83x 83x 52x 140x 47x 47x 47x 47x 47x 60x 60x 60x 47x 47x 47x 5x 16x 2x 2x 3x 14x 1x 1x 1x 1x 1x 1x 1x 140x 191x 329x       191x 191x 191x 329x 113x 113x 113x 113x 113x 113x 329x  
/** @import { Snapshot } from './types' */
import { DEV } from 'esm-env';
import * as w from './warnings.js';
import { get_prototype_of, is_array, object_prototype } from './utils.js';
 
/**
 * In dev, we keep track of which properties could not be cloned. In prod
 * we don't bother, but we keep a dummy array around so that the
 * signature stays the same
 * @type {string[]}
 */
const empty = [];
 
/**
 * @template T
 * @param {T} value
 * @param {boolean} [skip_warning]
 * @returns {Snapshot<T>}
 */
export function snapshot(value, skip_warning = false) {
	if (DEV) {
		/** @type {string[]} */
		const paths = [];
 
		const copy = clone(value, new Map(), '', paths);
		if (paths.length === 1 && paths[0] === '' && !skip_warning) {
			// value could not be cloned
			w.state_snapshot_uncloneable();
		} else if (paths.length > 0 && !skip_warning) {
			// some properties could not be cloned
			const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10);
			const excess = paths.length - slice.length;
 
			let uncloned = slice.map((path) => `- <value>${path}`).join('\n');
			if (excess > 0) uncloned += `\n- ...and ${excess} more`;
 
			w.state_snapshot_uncloneable(uncloned);
		}
 
		return copy;
	}

	return clone(value, new Map(), '', empty);
}
 
/**
 * @template T
 * @param {T} value
 * @param {Map<T, Snapshot<T>>} cloned
 * @param {string} path
 * @param {string[]} paths
 * @returns {Snapshot<T>}
 */
function clone(value, cloned, path, paths) {
	if (typeof value === 'object' && value !== null) {
		const unwrapped = cloned.get(value);
		if (unwrapped !== undefined) return unwrapped;
 
		if (is_array(value)) {
			const copy = /** @type {Snapshot<any>} */ ([]);
			cloned.set(value, copy);
 
			for (let i = 0; i < value.length; i += 1) {
				copy.push(clone(value[i], cloned, DEV ? `${path}[${i}]` : path, paths));
			}
 
			return copy;
		}
 
		if (get_prototype_of(value) === object_prototype) {
			/** @type {Snapshot<any>} */
			const copy = {};
			cloned.set(value, copy);
 
			for (var key in value) {
				// @ts-expect-error
				copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
			}
 
			return copy;
		}
 
		if (value instanceof Date) {
			return /** @type {Snapshot<T>} */ (structuredClone(value));
		}
 
		if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') {
			return clone(
				/** @type {T & { toJSON(): any } } */ (value).toJSON(),
				cloned,
				DEV ? `${path}.toJSON()` : path,
				paths
			);
		}
	}
 
	if (value instanceof EventTarget) {
		// can't be cloned
		return /** @type {Snapshot<T>} */ (value);
	}
 
	try {
		return /** @type {Snapshot<T>} */ (structuredClone(value));
	} catch (e) {
		if (DEV) {
			paths.push(path);
		}
 
		return /** @type {Snapshot<T>} */ (value);
	}
}