diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index e717077917..9e2fbd7db8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -128,12 +128,17 @@ export function VariableDeclaration(node, context) { const binding = /** @type {import('#compiler').Binding} */ ( context.state.scope.get(id.name) ); - if (rune === '$state' && should_proxy(value, context.state.scope)) { - value = b.call('$.proxy', value); + const is_state = is_state_source(binding, context.state.analysis); + const is_proxy = should_proxy(value, context.state.scope); + if (rune === '$state' && is_proxy) { + value = b.call('$.proxy', value, dev ? b.literal(id.name) : undefined); } - if (is_state_source(binding, context.state.analysis)) { + if (is_state) { value = b.call('$.state', value); } + if (dev && is_state) { + value = b.call('$.tag_source', value, b.literal(id.name)); + } return value; }; @@ -154,7 +159,11 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - return b.declarator(id, b.call('$.derived', expression)); + const call = b.call('$.derived', expression); + return b.declarator( + id, + dev ? b.call('$.tag_source', call, b.literal(id.name)) : call + ); }), ...paths.map((path) => { const value = /** @type {Expression} */ (context.visit(path.expression)); @@ -176,8 +185,13 @@ export function VariableDeclaration(node, context) { if (declarator.id.type === 'Identifier') { let expression = /** @type {Expression} */ (context.visit(value)); if (rune === '$derived') expression = b.thunk(expression); - - declarations.push(b.declarator(declarator.id, b.call('$.derived', expression))); + const call = b.call('$.derived', expression); + declarations.push( + b.declarator( + declarator.id, + dev ? b.call('$.tag_source', call, b.literal(declarator.id.name)) : call + ) + ); } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -205,7 +219,19 @@ export function VariableDeclaration(node, context) { for (const path of paths) { const expression = /** @type {Expression} */ (context.visit(path.expression)); - declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression)))); + const call = b.call('$.derived', b.thunk(expression)); + declarations.push( + b.declarator( + path.node, + dev + ? b.call( + '$.tag_source', + call, + b.literal(/** @type {Identifier} */ (path.node).name) + ) + : call + ) + ); } } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 7e5196c606..98cef658bf 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -25,3 +25,4 @@ export const EFFECT_IS_UPDATING = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); +export const PROXY_PATH_SYMBOL = Symbol('proxy path'); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index ad80e75c3d..a9d89e72c9 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -43,11 +43,14 @@ function log_entry(signal, entry) { const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state'; const current_reaction = /** @type {Reaction} */ (active_reaction); const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0; - + const { trace_name: name } = signal; + const style = dirty + ? 'color: CornflowerBlue; font-weight: bold' + : 'color: grey; font-weight: bold'; // eslint-disable-next-line no-console console.groupCollapsed( - `%c${type}`, - dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold', + typeof name === 'string' ? `%c${name} — ${type}` : `%c${type}`, + style, typeof value === 'object' && value !== null && STATE_SYMBOL in value ? snapshot(value, true) : value @@ -92,11 +95,9 @@ export function trace(label, fn) { var previously_tracing_expressions = tracing_expressions; try { tracing_expressions = { entries: new Map(), reaction: active_reaction }; - var start = performance.now(); var value = fn(); var time = (performance.now() - start).toFixed(2); - if (!effect_tracking()) { // eslint-disable-next-line no-console console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey'); @@ -177,3 +178,12 @@ export function get_stack(label) { } return error; } + +/** + * @param {Value} source + * @param {string} name + */ +export function tag_source(source, name) { + source.trace_name = name; + return source; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 71c06d7b1b..5770105144 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -7,7 +7,7 @@ export { add_locations } from './dev/elements.js'; export { hmr } from './dev/hmr.js'; export { create_ownership_validator } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; -export { trace } from './dev/tracing.js'; +export { trace, tag_source } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; export { validate_snippet_args } from './dev/validation.js'; export { await_block as await } from './dom/blocks/await.js'; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index fd5706eaf2..b261fc2750 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -12,15 +12,16 @@ import { state as source, set } from './reactivity/sources.js'; import { STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; -import { get_stack } from './dev/tracing.js'; +import { get_stack, tag_source } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; /** * @template T * @param {T} value + * @param {string} [path] * @returns {T} */ -export function proxy(value) { +export function proxy(value, path) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -39,6 +40,16 @@ export function proxy(value) { var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; var reaction = active_reaction; + /** @type {(prop: any) => any} */ + var to_trace_name = DEV + ? (prop) => { + return typeof prop === 'symbol' + ? `${path}[unique symbol]` + : typeof prop === 'number' || Number(prop) === Number(prop) + ? `${path}[${prop}]` + : `${path}.${prop}`; + } + : (prop) => undefined; /** * @template T @@ -58,7 +69,8 @@ export function proxy(value) { if (is_proxied_array) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy - sources.set('length', source(/** @type {any[]} */ (value).length, stack)); + const length_source = source(/** @type {any[]} */ (value).length, stack); + sources.set('length', DEV ? tag_source(length_source, to_trace_name('length')) : length_source); } return new Proxy(/** @type {any} */ (value), { @@ -80,11 +92,12 @@ export function proxy(value) { if (s === undefined) { s = with_parent(() => source(descriptor.value, stack)); + s = DEV && typeof prop === 'string' ? tag_source(s, to_trace_name(prop)) : s; sources.set(prop, s); } else { set( s, - with_parent(() => proxy(descriptor.value)) + with_parent(() => proxy(descriptor.value, to_trace_name(prop))) ); } @@ -96,9 +109,10 @@ export function proxy(value) { if (s === undefined) { if (prop in target) { + const s = with_parent(() => source(UNINITIALIZED, stack)); sources.set( prop, - with_parent(() => source(UNINITIALIZED, stack)) + DEV && typeof prop === 'string' ? tag_source(s, to_trace_name(prop)) : s ); update_version(version); } @@ -130,7 +144,10 @@ export function proxy(value) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { - s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack)); + s = with_parent(() => + source(proxy(exists ? target[prop] : UNINITIALIZED, to_trace_name(prop)), stack) + ); + s = DEV && typeof prop === 'string' ? tag_source(s, to_trace_name(prop)) : s; sources.set(prop, s); } @@ -178,7 +195,10 @@ export function proxy(value) { (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { - s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack)); + s = with_parent(() => + source(has ? proxy(target[prop], to_trace_name(prop)) : UNINITIALIZED, stack) + ); + s = DEV && typeof prop === 'string' ? tag_source(s, to_trace_name(prop)) : s; sources.set(prop, s); } @@ -206,6 +226,7 @@ export function proxy(value) { // else a later read of the property would result in a source being created with // the value of the original item at that index. other_s = with_parent(() => source(UNINITIALIZED, stack)); + other_s = DEV ? tag_source(other_s, to_trace_name(i)) : other_s; sources.set(i + '', other_s); } } @@ -218,9 +239,10 @@ export function proxy(value) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); + s = DEV && typeof prop === 'string' ? tag_source(s, to_trace_name(prop)) : s; set( s, - with_parent(() => proxy(value)) + with_parent(() => proxy(value, to_trace_name(prop))) ); sources.set(prop, s); } @@ -228,7 +250,7 @@ export function proxy(value) { has = s.v !== UNINITIALIZED; set( s, - with_parent(() => proxy(value)) + with_parent(() => proxy(value, to_trace_name(prop))) ); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9d2ad2baee..ad5ad3383d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -139,7 +139,7 @@ export function set(source, value, should_proxy = false) { e.state_unsafe_mutation(); } - let new_value = should_proxy ? proxy(value) : value; + let new_value = should_proxy ? proxy(value, DEV ? source.trace_name : undefined) : value; return internal_set(source, new_value); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 5ef0097649..74982cac91 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -21,6 +21,7 @@ export interface Value extends Signal { updated?: Error | null; trace_need_increase?: boolean; trace_v?: V; + trace_name?: string; debug?: null | (() => void); }