diff --git a/.changeset/big-teachers-agree.md b/.changeset/big-teachers-agree.md new file mode 100644 index 000000000000..f9044541d7b8 --- /dev/null +++ b/.changeset/big-teachers-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add source name logging to `$inspect.trace` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index a46b3186010c..ce190814f8d8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -67,11 +67,20 @@ function build_assignment(operator, left, right, context) { in_constructor: rune !== '$derived' && rune !== '$derived.by' }; - return b.assignment( - operator, - b.member(b.this, field.key), - /** @type {Expression} */ (context.visit(right, child_state)) - ); + let value = /** @type {Expression} */ (context.visit(right, child_state)); + + if (dev) { + const declaration = context.path.findLast( + (parent) => parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression' + ); + value = b.call( + '$.tag', + value, + b.literal(`${declaration?.id?.name ?? '[class]'}.${name}`) + ); + } + + return b.assignment(operator, b.member(b.this, field.key), value); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 71a8fc3d05ba..e78a8824ddbf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -1,7 +1,9 @@ -/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */ +/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */ /** @import { StateField } from '#compiler' */ /** @import { Context } from '../types' */ import * as b from '#compiler/builders'; +import { dev } from '../../../../state.js'; +import { get_parent } from '../../../../utils/ast.js'; import { get_name } from '../../../nodes.js'; /** @@ -50,6 +52,10 @@ export function ClassBody(node, context) { } } + const declaration = /** @type {ClassDeclaration | ClassExpression} */ ( + get_parent(context.path, -1) + ); + // Replace parts of the class body for (const definition of node.body) { if (definition.type !== 'PropertyDefinition') { @@ -68,17 +74,26 @@ export function ClassBody(node, context) { } if (name[0] === '#') { - body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); + let value = definition.value + ? /** @type {CallExpression} */ (context.visit(definition.value, child_state)) + : undefined; + + if (dev) { + value = b.call('$.tag', value, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`)); + } + + body.push(b.prop_def(definition.key, value)); } else if (field.node === definition) { - const member = b.member(b.this, field.key); + let call = /** @type {CallExpression} */ (context.visit(field.value, child_state)); + if (dev) { + call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`)); + } + const member = b.member(b.this, field.key); const should_proxy = field.type === '$state' && true; // TODO body.push( - b.prop_def( - field.key, - /** @type {CallExpression} */ (context.visit(field.value, child_state)) - ), + b.prop_def(field.key, call), b.method('get', definition.key, [], [b.return(b.call('$.get', member))]), 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 b8d692698dd7..53f18d42e43a 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 @@ -90,6 +90,10 @@ export function VariableDeclaration(node, context) { should_proxy(initial, context.state.scope) ) { initial = b.call('$.proxy', initial); + + if (dev) { + initial = b.call('$.tag_proxy', initial, b.literal(id.name)); + } } if (is_prop_source(binding, context.state)) { @@ -128,12 +132,25 @@ 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)) { + 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); + + if (dev && !is_state) { + value = b.call('$.tag_proxy', value, b.literal(id.name)); + } } - if (is_state_source(binding, context.state.analysis)) { + + if (is_state) { value = b.call('$.state', value); + + if (dev) { + value = b.call('$.tag', value, b.literal(id.name)); + } } + return value; }; @@ -154,7 +171,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', call, b.literal('[$state iterable]')) : call + ); }), ...paths.map((path) => { const value = /** @type {Expression} */ (context.visit(path.expression)); @@ -176,8 +197,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', call, b.literal(declarator.id.name)) : call + ) + ); } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -189,8 +215,10 @@ export function VariableDeclaration(node, context) { let expression = /** @type {Expression} */ (context.visit(value)); if (rune === '$derived') expression = b.thunk(expression); - - declarations.push(b.declarator(id, b.call('$.derived', expression))); + const call = b.call('$.derived', expression); + declarations.push( + b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) + ); } const { inserts, paths } = extract_paths(declarator.id, rhs); @@ -200,12 +228,23 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - declarations.push(b.declarator(id, b.call('$.derived', expression))); + const call = b.call('$.derived', expression); + declarations.push( + b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) + ); } 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', 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 7e5196c606b4..98cef658bf6c 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 ad80e75c3dff..18b99c31b0cf 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -2,7 +2,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; -import { DERIVED, STATE_SYMBOL } from '#client/constants'; +import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; @@ -43,11 +43,15 @@ 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 style = dirty + ? 'color: CornflowerBlue; font-weight: bold' + : 'color: grey; font-weight: normal'; // eslint-disable-next-line no-console console.groupCollapsed( - `%c${type}`, - dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold', + signal.label ? `%c${type}%c ${signal.label}` : `%c${type}%c`, + style, + dirty ? 'font-weight: normal' : style, typeof value === 'object' && value !== null && STATE_SYMBOL in value ? snapshot(value, true) : value @@ -177,3 +181,34 @@ export function get_stack(label) { } return error; } + +/** + * @param {Value} source + * @param {string} label + */ +export function tag(source, label) { + source.label = label; + tag_proxy(source.v, label); + + return source; +} + +/** + * @param {unknown} value + * @param {string} label + */ +export function tag_proxy(value, label) { + // @ts-expect-error + value?.[PROXY_PATH_SYMBOL]?.(label); + return value; +} + +/** + * @param {unknown} value + */ +export function label(value) { + if (typeof value === 'symbol') return `Symbol(${value.description})`; + if (typeof value === 'function') return ''; + if (typeof value === 'object' && value) return ''; + return String(value); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 71c06d7b1b8b..60f9af912060 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, tag_proxy } 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 fd5706eaf270..487050669933 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -9,12 +9,15 @@ import { object_prototype } from '../shared/utils.js'; import { state as source, set } from './reactivity/sources.js'; -import { STATE_SYMBOL } from '#client/constants'; +import { PROXY_PATH_SYMBOL, 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 } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; +// TODO move all regexes into shared module? +const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; + /** * @template T * @param {T} value @@ -61,6 +64,21 @@ export function proxy(value) { sources.set('length', source(/** @type {any[]} */ (value).length, stack)); } + /** Used in dev for $inspect.trace() */ + var path = ''; + + /** @param {string} new_path */ + function update_path(new_path) { + path = new_path; + + tag(version, `${path} version`); + + // rename all child sources and child proxies + for (const [prop, source] of sources) { + tag(source, get_label(path, prop)); + } + } + return new Proxy(/** @type {any} */ (value), { defineProperty(_, prop, descriptor) { if ( @@ -76,17 +94,20 @@ export function proxy(value) { e.state_descriptors_fixed(); } - var s = sources.get(prop); + with_parent(() => { + var s = sources.get(prop); - if (s === undefined) { - s = with_parent(() => source(descriptor.value, stack)); - sources.set(prop, s); - } else { - set( - s, - with_parent(() => proxy(descriptor.value)) - ); - } + if (s === undefined) { + s = source(descriptor.value, stack); + sources.set(prop, s); + + if (DEV && typeof prop === 'string') { + tag(s, get_label(path, prop)); + } + } else { + set(s, descriptor.value, true); + } + }); return true; }, @@ -96,11 +117,13 @@ export function proxy(value) { if (s === undefined) { if (prop in target) { - sources.set( - prop, - with_parent(() => source(UNINITIALIZED, stack)) - ); + const s = with_parent(() => source(UNINITIALIZED, stack)); + sources.set(prop, s); update_version(version); + + if (DEV) { + tag(s, get_label(path, prop)); + } } } else { // When working with arrays, we need to also ensure we update the length when removing @@ -125,12 +148,26 @@ export function proxy(value) { return value; } + if (DEV && prop === PROXY_PATH_SYMBOL) { + return update_path; + } + var s = sources.get(prop); var exists = prop in target; // 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(() => { + var p = proxy(exists ? target[prop] : UNINITIALIZED); + var s = source(p, stack); + + if (DEV) { + tag(s, get_label(path, prop)); + } + + return s; + }); + sources.set(prop, s); } @@ -178,7 +215,17 @@ 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(() => { + var p = has ? proxy(target[prop]) : UNINITIALIZED; + var s = source(p, stack); + + if (DEV) { + tag(s, get_label(path, prop)); + } + + return s; + }); + sources.set(prop, s); } @@ -207,6 +254,10 @@ export function proxy(value) { // the value of the original item at that index. other_s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(i + '', other_s); + + if (DEV) { + tag(other_s, get_label(path, i)); + } } } } @@ -217,19 +268,23 @@ export function proxy(value) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = with_parent(() => source(undefined, stack)); - set( - s, - with_parent(() => proxy(value)) - ); + s = with_parent(() => { + var s = source(undefined, stack); + set(s, proxy(value)); + return s; + }); + sources.set(prop, s); + + if (DEV) { + tag(s, get_label(path, prop)); + } } } else { has = s.v !== UNINITIALIZED; - set( - s, - with_parent(() => proxy(value)) - ); + + var p = with_parent(() => proxy(value)); + set(s, p); } var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); @@ -282,6 +337,16 @@ export function proxy(value) { }); } +/** + * @param {string} path + * @param {string | symbol} prop + */ +function get_label(path, prop) { + if (typeof prop === 'symbol') return `${path}[Symbol(${prop.description ?? ''})]`; + if (regex_is_valid_identifier.test(prop)) return `${path}.${prop}`; + return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`; +} + /** * @param {Source} signal * @param {1 | -1} [d] diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9d2ad2baee4e..ad7566f77218 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -31,7 +31,7 @@ import { } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; -import { get_stack } from '../dev/tracing.js'; +import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -141,6 +141,10 @@ export function set(source, value, should_proxy = false) { let new_value = should_proxy ? proxy(value) : value; + if (DEV) { + tag_proxy(new_value, /** @type {string} */ (source.label)); + } + 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 5ef0097649a4..c445ade8468d 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; + label?: string; debug?: null | (() => void); } diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 1c8e5f15c048..0f3bc6fb9f87 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -7,8 +7,10 @@ import { raf } from '../internal/client/timing.js'; import { is_date } from './utils.js'; import { set, source } from '../internal/client/reactivity/sources.js'; import { render_effect } from '../internal/client/reactivity/effects.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { deferred, noop } from '../internal/shared/utils.js'; +import { DEV } from 'esm-env'; /** * @template T @@ -172,8 +174,8 @@ export class Spring { #damping = source(0.8); #precision = source(0.01); - #current = source(/** @type {T} */ (undefined)); - #target = source(/** @type {T} */ (undefined)); + #current; + #target; #last_value = /** @type {T} */ (undefined); #last_time = 0; @@ -192,11 +194,20 @@ export class Spring { * @param {SpringOpts} [options] */ constructor(value, options = {}) { - this.#current.v = this.#target.v = value; + this.#current = DEV ? tag(source(value), 'Spring.current') : source(value); + this.#target = DEV ? tag(source(value), 'Spring.target') : source(value); if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1); if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1); if (typeof options.precision === 'number') this.#precision.v = options.precision; + + if (DEV) { + tag(this.#stiffness, 'Spring.stiffness'); + tag(this.#damping, 'Spring.damping'); + tag(this.#precision, 'Spring.precision'); + tag(this.#current, 'Spring.current'); + tag(this.#target, 'Spring.target'); + } } /** diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 31cddf8f30f0..09bd06c325d5 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -7,7 +7,9 @@ import { loop } from '../internal/client/loop.js'; import { linear } from '../easing/index.js'; import { is_date } from './utils.js'; import { set, source } from '../internal/client/reactivity/sources.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { get, render_effect } from 'svelte/internal/client'; +import { DEV } from 'esm-env'; /** * @template T @@ -175,8 +177,8 @@ export function tweened(value, defaults = {}) { * @since 5.8.0 */ export class Tween { - #current = source(/** @type {T} */ (undefined)); - #target = source(/** @type {T} */ (undefined)); + #current; + #target; /** @type {TweenedOptions} */ #defaults; @@ -189,8 +191,14 @@ export class Tween { * @param {TweenedOptions} options */ constructor(value, options = {}) { - this.#current.v = this.#target.v = value; + this.#current = source(value); + this.#target = source(value); this.#defaults = options; + + if (DEV) { + tag(this.#current, 'Tween.current'); + tag(this.#target, 'Tween.target'); + } } /** diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 63deca62ea8b..491ffb45cba7 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -1,7 +1,9 @@ import { get, tick, untrack } from '../internal/client/runtime.js'; import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; import { source } from '../internal/client/reactivity/sources.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { increment } from './utils.js'; +import { DEV } from 'esm-env'; /** * Returns a `subscribe` function that, if called in an effect (including expressions in the template), @@ -51,6 +53,10 @@ export function createSubscriber(start) { /** @type {(() => void) | void} */ let stop; + if (DEV) { + tag(version, 'createSubscriber version'); + } + return () => { if (effect_tracking()) { get(version); diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js index 721673bc36f3..4176f0ceec3f 100644 --- a/packages/svelte/src/reactivity/date.js +++ b/packages/svelte/src/reactivity/date.js @@ -1,7 +1,9 @@ /** @import { Source } from '#client' */ import { derived } from '../internal/client/index.js'; import { source, set } from '../internal/client/reactivity/sources.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js'; +import { DEV } from 'esm-env'; var inited = false; @@ -49,6 +51,11 @@ export class SvelteDate extends Date { constructor(...params) { // @ts-ignore super(...params); + + if (DEV) { + tag(this.#time, 'SvelteDate.#time'); + } + if (!inited) this.#init(); } diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 3ae8fe5ad19c..eed163dbf29b 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -1,6 +1,7 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; import { set, source } from '../internal/client/reactivity/sources.js'; +import { label, tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { increment } from './utils.js'; @@ -62,8 +63,13 @@ export class SvelteMap extends Map { constructor(value) { super(); - // If the value is invalid then the native exception will fire here - if (DEV) value = new Map(value); + if (DEV) { + // If the value is invalid then the native exception will fire here + value = new Map(value); + + tag(this.#version, 'SvelteMap version'); + tag(this.#size, 'SvelteMap.size'); + } if (value) { for (var [key, v] of value) { @@ -82,6 +88,11 @@ export class SvelteMap extends Map { var ret = super.get(key); if (ret !== undefined) { s = source(0); + + if (DEV) { + tag(s, `SvelteMap get(${label(key)})`); + } + sources.set(key, s); } else { // We should always track the version in case @@ -113,6 +124,11 @@ export class SvelteMap extends Map { var ret = super.get(key); if (ret !== undefined) { s = source(0); + + if (DEV) { + tag(s, `SvelteMap get(${label(key)})`); + } + sources.set(key, s); } else { // We should always track the version in case @@ -138,7 +154,13 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - sources.set(key, source(0)); + s = source(0); + + if (DEV) { + tag(s, `SvelteMap get(${label(key)})`); + } + + sources.set(key, s); set(this.#size, super.size); increment(version); } else if (prev_res !== value) { @@ -197,12 +219,18 @@ export class SvelteMap extends Map { if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { - sources.set(key, source(0)); + var s = source(0); + + if (DEV) { + tag(s, `SvelteMap get(${label(key)})`); + } + + sources.set(key, s); } } } - for (var [, s] of this.#sources) { + for ([, s] of this.#sources) { get(s); } } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 4a0b4dfdb398..fd22014cb3f6 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,6 +1,7 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; import { source, set } from '../internal/client/reactivity/sources.js'; +import { label, tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { increment } from './utils.js'; @@ -56,8 +57,13 @@ export class SvelteSet extends Set { constructor(value) { super(); - // If the value is invalid then the native exception will fire here - if (DEV) value = new Set(value); + if (DEV) { + // If the value is invalid then the native exception will fire here + value = new Set(value); + + tag(this.#version, 'SvelteSet version'); + tag(this.#size, 'SvelteSet.size'); + } if (value) { for (var element of value) { @@ -111,6 +117,11 @@ export class SvelteSet extends Set { } s = source(true); + + if (DEV) { + tag(s, `SvelteSet has(${label(value)})`); + } + sources.set(value, s); } diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index c1a8275f150b..c77ff9c8225c 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -1,4 +1,6 @@ +import { DEV } from 'esm-env'; import { source } from '../internal/client/reactivity/sources.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { get_current_url } from './url.js'; import { increment } from './utils.js'; @@ -32,7 +34,7 @@ export const REPLACE = Symbol(); * ``` */ export class SvelteURLSearchParams extends URLSearchParams { - #version = source(0); + #version = DEV ? tag(source(0), 'SvelteURLSearchParams version') : source(0); #url = get_current_url(); #updating = false; diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js index 879006f057dc..56732a040247 100644 --- a/packages/svelte/src/reactivity/url.js +++ b/packages/svelte/src/reactivity/url.js @@ -1,4 +1,6 @@ +import { DEV } from 'esm-env'; import { source, set } from '../internal/client/reactivity/sources.js'; +import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { REPLACE, SvelteURLSearchParams } from './url-search-params.js'; @@ -56,6 +58,17 @@ export class SvelteURL extends URL { url = new URL(url, base); super(url); + if (DEV) { + tag(this.#protocol, 'SvelteURL.protocol'); + tag(this.#username, 'SvelteURL.username'); + tag(this.#password, 'SvelteURL.password'); + tag(this.#hostname, 'SvelteURL.hostname'); + tag(this.#port, 'SvelteURL.port'); + tag(this.#pathname, 'SvelteURL.pathname'); + tag(this.#hash, 'SvelteURL.hash'); + tag(this.#search, 'SvelteURL.search'); + } + current_url = this; this.#searchParams = new SvelteURLSearchParams(url.searchParams); current_url = null; diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index 8c50a5c440df..d4fcf29e4e12 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -1,8 +1,9 @@ -import { BROWSER } from 'esm-env'; +import { BROWSER, DEV } from 'esm-env'; import { on } from '../../events/index.js'; import { ReactiveValue } from '../reactive-value.js'; import { get } from '../../internal/client/index.js'; import { set, source } from '../../internal/client/reactivity/sources.js'; +import { tag } from '../../internal/client/dev/tracing.js'; /** * `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`. @@ -147,6 +148,10 @@ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { if (BROWSER) { this.#update(); } + + if (DEV) { + tag(this.#dpr, 'window.devicePixelRatio'); + } } get current() { diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index e62b662372da..2d825dbb7cbf 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -192,3 +192,44 @@ if (typeof window !== 'undefined') { } export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html'; + +/** + * @param {any[]} logs + */ +export function normalise_trace_logs(logs) { + let normalised = []; + + logs = logs.slice(); + + while (logs.length > 0) { + const log = logs.shift(); + + if (log instanceof Error) { + continue; + } + + if (typeof log === 'string' && log.includes('%c')) { + const split = log.split('%c'); + + const first = /** @type {string} */ (split.shift()).trim(); + if (first) normalised.push({ log: first }); + + while (split.length > 0) { + const log = /** @type {string} */ (split.shift()).trim(); + const highlighted = logs.shift() === 'color: CornflowerBlue; font-weight: bold'; + + // omit timings, as they will differ between runs + if (/\(.+ms\)/.test(log)) continue; + + normalised.push({ + log, + highlighted + }); + } + } else { + normalised.push({ log }); + } + } + + return normalised; +} diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js index f54f78f5c1f3..5957d2cc6aef 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,10 +11,11 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'iife', highlighted: false }, + { log: 'iife' }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 0 }, - { log: 'effect', highlighted: false } + { log: 'effect' } ]); logs.length = 0; @@ -47,10 +25,11 @@ export default test({ flushSync(); assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'iife', highlighted: false }, + { log: 'iife' }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 1 }, - { log: 'effect', highlighted: false } + { log: 'effect' } ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js index c9a66289a12d..683f55f32136 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,8 +11,9 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -52,20 +30,26 @@ export default test({ // checked changed, effect reassign state, values should be correct and be correctly highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 1 }, - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 2 }, - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 3 } ]); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js index efa5985e4e56..8e9204c90f2f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,12 +11,15 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 0 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 0 }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -52,12 +32,15 @@ export default test({ // count changed, derived and state are highlighted, last state is not assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 2 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 1 }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -70,12 +53,15 @@ export default test({ // checked changed, last state is highlighted, first two are not assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: false }, + { log: 'double', highlighted: false }, { log: 2 }, { log: '$state', highlighted: false }, + { log: 'count', highlighted: false }, { log: 1 }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: true } ]); @@ -87,10 +73,12 @@ export default test({ // count change and derived it's >=4, checked is not in the dependencies anymore assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 4 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 2 } ]); }