All files / src/internal/client/dev ownership.js

96.53% Statements 167/173
84.84% Branches 28/33
100% Functions 9/9
96.49% Lines 165/171

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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 1722x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 879x 879x 879x 879x 879x 879x 879x 9669x 9669x 9669x 8678x 8678x 8678x 8678x 8678x 8678x 9669x 879x 879x 879x 2x 2x 2x 2x 2x 749x 749x 749x 749x 749x 749x 749x 749x 749x 737x 737x 737x 737x 737x 737x 737x     12x 12x 12x 12x 12x 12x         2x 2x 2x 2x 2x 2x 2x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 2x 2x 2x 2x 2x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 2x 2x 2x 2x 2x 2x 2x 18x 18x 18x 18x 2x 2x 2x 2x 2x 20x 20x 4x 4x 4x 2x 2x 4x 20x 2x 2x 2x 2x 2x 399x 399x 399x 399x 2x 2x 2x 2x 399x 399x 26x 26x 26x 32x 32x 26x 399x 2x 2x 2x 2x 2x 749x 749x 749x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 749x  
/** @typedef {{ file: string, line: number, column: number }} Location */
 
import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js';
 
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
 
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/;
 
function get_stack() {
	const stack = new Error().stack;
	if (!stack) return null;
 
	const entries = [];
 
	for (const line of stack.split('\n')) {
		let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
 
		if (match) {
			entries.push({
				file: match[1],
				line: +match[2],
				column: +match[3]
			});
		}
	}
 
	return entries;
}
 
/**
 * Determines which `.svelte` component is responsible for a given state change
 * @returns {Function | null}
 */
function get_component() {
	// first 4 lines are svelte internals; adjust this number if we change the internal call stack
	const stack = get_stack()?.slice(4);
	if (!stack) return null;
 
	for (let i = 0; i < stack.length; i++) {
		const entry = stack[i];
		const modules = boundaries[entry.file];
		if (!modules) {
			// If the first entry is not a component, that means the modification very likely happened
			// within a .svelte.js file, possibly triggered by a component. Since these files are not part
			// of the bondaries/component context heuristic, we need to bail in this case, else we would
			// have false positives when the .svelte.ts file provides a state creator function, encapsulating
			// the state and its mutations, and is being called from a component other than the one who
			// called the state creator function.
			if (i === 0) return null;
			continue;
		}
 
		for (const module of modules) {
			if (module.start.line < entry.line && module.end.line > entry.line) {
				return module.component;
			}
		}
	}

	return null;
}
 
/**
 * Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
 * such that subsequent calls to `get_component` can tell us which component is responsible
 * for a given state change
 */
export function mark_module_start() {
	const start = get_stack()?.[2];
 
	if (start) {
		(boundaries[start.file] ??= []).push({
			start,
			// @ts-expect-error
			end: null,
			// @ts-expect-error we add the component at the end, since HMR will overwrite the function
			component: null
		});
	}
}
 
/**
 * @param {Function} component
 */
export function mark_module_end(component) {
	const end = get_stack()?.[2];
 
	if (end) {
		const boundaries_file = boundaries[end.file];
		const boundary = boundaries_file[boundaries_file.length - 1];
 
		boundary.end = end;
		boundary.component = component;
	}
}
 
/**
 *
 * @param {any} object
 * @param {any} owner
 */
export function add_owner(object, owner) {
	untrack(() => {
		add_owner_to_object(object, owner);
	});
}
 
/**
 * @param {any} object
 * @param {Function} owner
 */
function add_owner_to_object(object, owner) {
	if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
		object[STATE_SYMBOL].o.add(owner);
 
		for (const key in object) {
			add_owner_to_object(object[key], owner);
		}
	}
}
 
/**
 * @param {any} object
 */
export function strip_owner(object) {
	untrack(() => {
		strip_owner_from_object(object);
	});
}
 
/**
 * @param {any} object
 */
function strip_owner_from_object(object) {
	if (object?.[STATE_SYMBOL]?.o) {
		object[STATE_SYMBOL].o = null;
 
		for (const key in object) {
			strip_owner(object[key]);
		}
	}
}
 
/**
 * @param {Set<Function>} owners
 */
export function check_ownership(owners) {
	const component = get_component();
 
	if (component && !owners.has(component)) {
		let original = [...owners][0];
 
		let message =
			// @ts-expect-error
			original.filename !== component.filename
				? // @ts-expect-error
					`${component.filename} mutated a value owned by ${original.filename}. This is strongly discouraged`
				: 'Mutating a value outside the component that created it is strongly discouraged';
 
		// eslint-disable-next-line no-console
		console.warn(
			`${message}. Consider passing values to child components with \`bind:\`, or use a callback instead.`
		);
 
		// eslint-disable-next-line no-console
		console.trace();
	}
}