Skip to content

chore: refactor $inspect #11226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/svelte/src/internal/client/dev/inspect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { snapshot } from '../proxy.js';
import { render_effect } from '../reactivity/effects.js';
import { current_effect, deep_read } from '../runtime.js';
import { array_prototype, get_prototype_of, object_prototype } from '../utils.js';

/** @type {Function | null} */
export let inspect_fn = null;

/** @param {Function | null} fn */
export function set_inspect_fn(fn) {
inspect_fn = fn;
}

/** @type {Array<import('#client').ValueDebug>} */
export let inspect_captured_signals = [];

/**
* @param {() => any[]} get_value
* @param {Function} [inspector]
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspector = console.log) {
if (!current_effect) {
throw new Error(
'$inspect can only be used inside an effect (e.g. during component initialisation)'
);
}

let initial = true;

// we assign the function directly to signals, rather than just
// calling `inspector` directly inside the effect, so that
// we get useful stack traces
var fn = () => {
const value = deep_snapshot(get_value());
inspector(initial ? 'init' : 'update', ...value);
};

render_effect(() => {
inspect_fn = fn;
deep_read(get_value());
inspect_fn = null;

const signals = inspect_captured_signals.slice();
inspect_captured_signals = [];

if (initial) {
fn();
initial = false;
}

return () => {
for (const s of signals) {
s.inspect.delete(fn);
}
};
});
}

/**
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = snapshot(value);

if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}

const prototype = get_prototype_of(value);

// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};

for (let key in value) {
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}

visited.set(value, contains_unstated ? nested_unstated : value);
} else {
visited.set(value, value);
}
}

return visited.get(value) ?? value;
}
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { hmr } from './dev/hmr.js';
export { ADD_OWNER, add_owner, mark_module_start, mark_module_end } from './dev/ownership.js';
export { inspect } from './dev/inspect.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
Expand Down Expand Up @@ -111,7 +112,6 @@ export {
exclude_from_object,
pop,
push,
inspect,
unwrap,
freeze,
deep_read,
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/client/reactivity/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
import { get_descriptor, is_function } from '../utils.js';
import { mutable_source, set } from './sources.js';
import { derived } from './deriveds.js';
import { get, inspect_fn, is_signals_recorded, untrack } from '../runtime.js';
import { get, is_signals_recorded, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import { inspect_fn } from '../dev/inspect.js';

/**
* @param {((value?: number) => number)} fn
Expand Down
116 changes: 9 additions & 107 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import { DEV } from 'esm-env';
import {
array_prototype,
get_descriptors,
get_prototype_of,
is_frozen,
object_freeze,
object_prototype
} from './utils.js';
import { get_descriptors, get_prototype_of, is_frozen, object_freeze } from './utils.js';
import { snapshot } from './proxy.js';
import {
destroy_effect,
effect,
execute_effect_teardown,
user_pre_effect
} from './reactivity/effects.js';
import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js';
import {
EFFECT,
RENDER_EFFECT,
Expand All @@ -33,6 +21,7 @@ import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
import { mutate, set, source } from './reactivity/sources.js';
import { update_derived } from './reactivity/deriveds.js';
import { inspect_captured_signals, inspect_fn, set_inspect_fn } from './dev/inspect.js';

const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
Expand Down Expand Up @@ -115,12 +104,6 @@ export let current_skip_reaction = false;
export let is_signals_recorded = false;
let captured_signals = new Set();

/** @type {Function | null} */
export let inspect_fn = null;

/** @type {Array<import('./types.js').ValueDebug>} */
let inspect_captured_signals = [];

// Handling runtime component context
/** @type {import('./types.js').ComponentContext | null} */
export let current_component_context = null;
Expand Down Expand Up @@ -700,11 +683,10 @@ export async function tick() {
* @returns {V}
*/
export function get(signal) {
// @ts-expect-error
if (DEV && signal.inspect && inspect_fn) {
/** @type {import('./types.js').ValueDebug} */ (signal).inspect.add(inspect_fn);
// @ts-expect-error
inspect_captured_signals.push(signal);
if (DEV && inspect_fn) {
var s = /** @type {import('#client').ValueDebug} */ (signal);
s.inspect.add(inspect_fn);
inspect_captured_signals.push(s);
}

const flags = signal.f;
Expand Down Expand Up @@ -761,9 +743,9 @@ export function get(signal) {
if (DEV) {
// we want to avoid tracking indirect dependencies
const previous_inspect_fn = inspect_fn;
inspect_fn = null;
set_inspect_fn(null);
update_derived(/** @type {import('./types.js').Derived} **/ (signal), false);
inspect_fn = previous_inspect_fn;
set_inspect_fn(previous_inspect_fn);
} else {
update_derived(/** @type {import('./types.js').Derived} **/ (signal), false);
}
Expand Down Expand Up @@ -1186,86 +1168,6 @@ export function deep_read(value, visited = new Set()) {
}
}

/**
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = snapshot(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
const prototype = get_prototype_of(value);
// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}
visited.set(value, contains_unstated ? nested_unstated : value);
} else {
visited.set(value, value);
}
}

return visited.get(value) ?? value;
}

// TODO remove in a few versions, before 5.0 at the latest
let warned_inspect_changed = false;

/**
* @param {() => any[]} get_value
* @param {Function} [inspect]
*/
// eslint-disable-next-line no-console
export function inspect(get_value, inspect = console.log) {
let initial = true;

user_pre_effect(() => {
const fn = () => {
const value = untrack(() => get_value().map((v) => deep_snapshot(v)));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console
console.warn(
'$inspect() API has changed. See https://svelte-5-preview.vercel.app/docs/runes#$inspect for more information.'
);
warned_inspect_changed = true;
}
inspect(initial ? 'init' : 'update', ...value);
};

inspect_fn = fn;
const value = get_value();
deep_read(value);
inspect_fn = null;

const signals = inspect_captured_signals.slice();
inspect_captured_signals = [];

if (initial) {
fn();
initial = false;
}

return () => {
for (const s of signals) {
s.inspect.delete(fn);
}
};
});
}

/**
* @template V
* @param {V | import('#client').Value<V>} value
Expand Down