From 307f15d5f78e246de6523a71f0869e32e6b86a37 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 18 Apr 2024 12:02:09 -0400 Subject: [PATCH] chore: refactor `$inspect` (#11226) * chore: move inspect logic into its own module * better error * remove unused imports --- .../svelte/src/internal/client/dev/inspect.js | 98 +++++++++++++++ packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/props.js | 3 +- .../svelte/src/internal/client/runtime.js | 116 ++---------------- 4 files changed, 110 insertions(+), 109 deletions(-) create mode 100644 packages/svelte/src/internal/client/dev/inspect.js diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js new file mode 100644 index 0000000000..8e3c11dd63 --- /dev/null +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -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} */ +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} 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; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 18bd4740c8..2362698ab1 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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'; @@ -111,7 +112,6 @@ export { exclude_from_object, pop, push, - inspect, unwrap, freeze, deep_read, diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 4fd6fcad8a..053427ddd2 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -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 diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fe93eb47e4..88b7110b99 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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, @@ -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; @@ -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} */ -let inspect_captured_signals = []; - // Handling runtime component context /** @type {import('./types.js').ComponentContext | null} */ export let current_component_context = null; @@ -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; @@ -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); } @@ -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} 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} value