All files / src/internal/shared clone.js

94.49% Statements 103/109
83.87% Branches 26/31
100% Functions 2/2
94.33% Lines 100/106

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 1072x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 63x 63x 63x 63x 63x 63x 1x 1x 63x 2x 2x 2x 2x 2x 2x 2x 2x 2x 63x 63x 63x       2x 2x 2x 2x 2x 2x 2x 2x 2x 299x 299x 120x 120x 115x 120x 73x 73x 73x 73x 187x 187x 73x 73x 73x 42x 46x 39x 39x 39x 39x 39x 48x 48x 48x 39x 39x 39x 3x 14x 1x 1x 1x 1x 1x 1x 1x 120x 181x 299x       181x 181x 181x 299x 111x 111x 111x 111x 111x 111x 299x  
/** @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
 * @returns {Snapshot<T>}
 */
export function snapshot(value) {
	if (DEV) {
		/** @type {string[]} */
		const paths = [];
 
		const copy = clone(value, new Map(), '', paths);
		if (paths.length === 1 && paths[0] === '') {
			// value could not be cloned
			w.state_snapshot_uncloneable();
		} else if (paths.length > 0) {
			// 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 (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);
	}
}