diff --git a/.changeset/flat-points-kick.md b/.changeset/flat-points-kick.md deleted file mode 100644 index f9c8b8762c..0000000000 --- a/.changeset/flat-points-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index e19e68f6a8..8e6c91fad7 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -20,7 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates. -State is proxified recursively until Svelte finds something other than an array or simple object (like a class). In a case like this... +State is proxified recursively until Svelte finds something other than an array or simple object (like a class or an object created with `Object.create`). In a case like this... ```js let todos = $state([ diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 506f04c1c4..34e1c2fc6e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,25 @@ # svelte +## 5.34.0 + +### Minor Changes + +- feat: add source name logging to `$inspect.trace` ([#16060](https://github.com/sveltejs/svelte/pull/16060)) + +### Patch Changes + +- fix: add `command` and `commandfor` to `HTMLButtonAttributes` ([#16117](https://github.com/sveltejs/svelte/pull/16117)) + +- fix: better `$inspect.trace()` output ([#16131](https://github.com/sveltejs/svelte/pull/16131)) + +- fix: properly hydrate dynamic css props components and remove element removal ([#16118](https://github.com/sveltejs/svelte/pull/16118)) + +## 5.33.19 + +### Patch Changes + +- fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect ([#16119](https://github.com/sveltejs/svelte/pull/16119)) + ## 5.33.18 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 8a1bdd0e6d..076cc71b38 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -926,6 +926,17 @@ export interface HTMLButtonAttributes extends HTMLAttributes value?: string | string[] | number | undefined | null; popovertarget?: string | undefined | null; popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null; + command?: + | 'show-modal' + | 'close' + | 'request-close' + | 'show-popover' + | 'hide-popover' + | 'toggle-popover' + | (string & {}) + | undefined + | null; + commandfor?: string | undefined | null; } export interface HTMLCanvasAttributes extends HTMLAttributes { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6702b937f9..3ecb1c42f5 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.33.18", + "version": "5.34.0", "type": "module", "types": "./types/index.d.ts", "engines": { 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 a46b318601..ce190814f8 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 71a8fc3d05..e78a8824dd 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/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js index 783bc38e3c..d58a24b455 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -1,7 +1,6 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ -import * as b from '#compiler/builders'; +import { regex_is_valid_identifier } from '../../../patterns.js'; import { build_component } from './shared/component.js'; /** @@ -9,24 +8,12 @@ import { build_component } from './shared/component.js'; * @param {ComponentContext} context */ export function Component(node, context) { - if (node.metadata.dynamic) { - // Handle dynamic references to what seems like static inline components - const component = build_component(node, '$$component', context, b.id('$$anchor')); - context.state.init.push( - b.stmt( - b.call( - '$.component', - context.state.node, - // TODO use untrack here to not update when binding changes? - // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this - b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))), - b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component])) - ) - ) - ); - return; - } - - const component = build_component(node, node.name, context); + const component = build_component( + node, + // if it's not dynamic we will just use the node name, if it is dynamic we will use the node name + // only if it's a valid identifier, otherwise we will use a default name + !node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component', + context + ); context.state.init.push(component); } 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 06dad23dec..26362b8f44 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)); @@ -203,7 +224,10 @@ export function VariableDeclaration(node, context) { let expression = /** @type {Expression} */ (context.visit(value)); if (rune === '$derived') expression = b.thunk(expression); - declarations.push(b.declarator(declarator.id, b.call('$.derived', expression))); + let call = b.call('$.derived', expression); + if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); + + declarations.push(b.declarator(declarator.id, call)); } } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -216,8 +240,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); @@ -227,12 +253,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/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index b4db8cb3a8..bdfb71152c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -14,10 +14,13 @@ import { create_derived } from '../../utils.js'; * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node * @param {string} component_name * @param {ComponentContext} context - * @param {Expression} anchor * @returns {Statement} */ -export function build_component(node, component_name, context, anchor = context.state.node) { +export function build_component(node, component_name, context) { + /** + * @type {Expression} + */ + const anchor = context.state.node; /** @type {Array} */ const props_and_spreads = []; /** @type {Array<() => void>} */ @@ -435,7 +438,7 @@ export function build_component(node, component_name, context, anchor = context. // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // will be handled separately through the `$.component` function, and then the component name will // always be referenced through just the identifier here. - node.type === 'SvelteComponent' + node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic) ? component_name : /** @type {Expression} */ (context.visit(b.member_id(component_name))), node_id, @@ -458,14 +461,18 @@ export function build_component(node, component_name, context, anchor = context. ) ]; - if (node.type === 'SvelteComponent') { + if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) { const prev = fn; fn = (node_id) => { return b.call( '$.component', node_id, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))), + b.thunk( + /** @type {Expression} */ ( + context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression) + ) + ), b.arrow( [b.id('$$anchor'), b.id(component_name)], b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 44a8839d98..47cf48b366 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -30,6 +30,7 @@ export const EFFECT_ASYNC = 1 << 25; export const ASYNC_ERROR = 1; export const STATE_SYMBOL = Symbol('$state'); +export const PROXY_PATH_SYMBOL = Symbol('proxy path'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index ad80e75c3d..5834f5bffd 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -2,52 +2,42 @@ 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'; -/** @type { any } */ +/** + * @typedef {{ + * traces: Error[]; + * }} TraceEntry + */ + +/** @type {{ reaction: Reaction | null, entries: Map } | null} */ export let tracing_expressions = null; /** - * @param { Value } signal - * @param { { read: Error[] } } [entry] + * @param {Value} signal + * @param {TraceEntry} [entry] */ function log_entry(signal, entry) { - const debug = signal.debug; - const value = signal.trace_need_increase ? signal.trace_v : signal.v; + const value = signal.v; if (value === UNINITIALIZED) { return; } - if (debug) { - var previous_captured_signals = captured_signals; - var captured = new Set(); - set_captured_signals(captured); - try { - untrack(() => { - debug(); - }); - } finally { - set_captured_signals(previous_captured_signals); - } - if (captured.size > 0) { - for (const dep of captured) { - log_entry(dep); - } - return; - } - } - 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 @@ -65,17 +55,15 @@ function log_entry(signal, entry) { console.log(signal.created); } - if (signal.updated) { + if (dirty && signal.updated) { // eslint-disable-next-line no-console console.log(signal.updated); } - const read = entry?.read; - - if (read && read.length > 0) { - for (var stack of read) { + if (entry) { + for (var trace of entry.traces) { // eslint-disable-next-line no-console - console.log(stack); + console.log(trace); } } @@ -90,6 +78,7 @@ function log_entry(signal, entry) { */ export function trace(label, fn) { var previously_tracing_expressions = tracing_expressions; + try { tracing_expressions = { entries: new Map(), reaction: active_reaction }; @@ -97,39 +86,32 @@ export function trace(label, fn) { var value = fn(); var time = (performance.now() - start).toFixed(2); + var prefix = untrack(label); + if (!effect_tracking()) { // eslint-disable-next-line no-console - console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey'); + console.log(`${prefix} %cran outside of an effect (${time}ms)`, 'color: grey'); } else if (tracing_expressions.entries.size === 0) { // eslint-disable-next-line no-console - console.log(`${label()} %cno reactive dependencies (${time}ms)`, 'color: grey'); + console.log(`${prefix} %cno reactive dependencies (${time}ms)`, 'color: grey'); } else { // eslint-disable-next-line no-console - console.group(`${label()} %c(${time}ms)`, 'color: grey'); + console.group(`${prefix} %c(${time}ms)`, 'color: grey'); var entries = tracing_expressions.entries; + untrack(() => { + for (const [signal, traces] of entries) { + log_entry(signal, traces); + } + }); + tracing_expressions = null; - for (const [signal, entry] of entries) { - log_entry(signal, entry); - } // eslint-disable-next-line no-console console.groupEnd(); } - if (previously_tracing_expressions !== null && tracing_expressions !== null) { - for (const [signal, entry] of tracing_expressions.entries) { - var prev_entry = previously_tracing_expressions.get(signal); - - if (prev_entry === undefined) { - previously_tracing_expressions.set(signal, entry); - } else { - prev_entry.read.push(...entry.read); - } - } - } - return value; } finally { tracing_expressions = previously_tracing_expressions; @@ -177,3 +159,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/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index ecbcfd3e83..ef36198753 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -26,8 +26,4 @@ export function css_props(element, get_styles) { } } }); - - teardown(() => { - element.remove(); - }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index b5590a8553..3144233df8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -642,7 +642,7 @@ function create_item( if (DEV && reactive) { // For tracing purposes, we need to link the source signal we create with the // collection + index so that tracing works as intended - /** @type {Value} */ (v).debug = () => { + /** @type {Value} */ (v).trace = () => { var collection_index = typeof i === 'number' ? index : i.v; // eslint-disable-next-line @typescript-eslint/no-unused-expressions get_collection()[collection_index]; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 7becf49e21..c300f00b3d 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 { async } from './dom/blocks/async.js'; export { validate_snippet_args } from './dev/validation.js'; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index fd5706eaf2..4870506699 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 69967ab3b9..2398cd48bc 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -32,13 +32,15 @@ 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 { Batch } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; export let inspect_effects = new Set(); + +/** @type {Map} */ export const old_values = new Map(); /** Internal representation of `$effect.pending()` */ @@ -71,7 +73,9 @@ export function source(v, stack) { if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); - signal.debug = null; + signal.updated = null; + signal.set_during_effect = false; + signal.trace = null; } return signal; @@ -146,6 +150,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); } @@ -174,9 +182,9 @@ export function internal_set(source, value) { if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); - if (active_effect != null) { - source.trace_need_increase = true; - source.trace_v ??= old_value; + + if (active_effect !== null) { + source.set_during_effect = true; } } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 5af392c791..c24ecdbdc9 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -17,12 +17,21 @@ export interface Value extends Signal { rv: number; /** The latest value for this signal */ v: V; - /** Dev only */ + + // dev-only + /** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */ + label?: string; + /** An error with a stack trace showing when the source was created */ created?: Error | null; + /** An error with a stack trace showing when the source was last updated */ updated?: Error | null; - trace_need_increase?: boolean; - trace_v?: V; - debug?: null | (() => void); + /** + * Whether or not the source was set while running an effect — if so, we need to + * increment the write version so that it shows up as dirty when the effect re-runs + */ + set_during_effect?: boolean; + /** A function that retrieves the underlying source, used for each block item signals */ + trace?: null | (() => void); } export interface Reaction extends Signal { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index bea9a729d7..78b38912e2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -57,6 +57,7 @@ import { import * as w from './warnings.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; +import { snapshot } from '../shared/clone.js'; /** @type {Effect | null} */ let last_scheduled_effect = null; @@ -480,19 +481,13 @@ export function update_effect(effect) { effect.teardown = typeof teardown === 'function' ? teardown : null; effect.wv = write_version; - var deps = effect.deps; - - // In DEV, we need to handle a case where $inspect.trace() might - // incorrectly state a source dependency has not changed when it has. - // That's beacuse that source was changed by the same effect, causing - // the versions to match. We can avoid this by incrementing the version - if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && deps !== null) { - for (let i = 0; i < deps.length; i++) { - var dep = deps[i]; - if (dep.trace_need_increase) { + // In DEV, increment versions of any sources that were written to during the effect, + // so that they are correctly marked as dirty when the effect re-runs + if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) { + for (var dep of effect.deps) { + if (dep.set_during_effect) { dep.wv = increment_write_version(); - dep.trace_need_increase = undefined; - dep.trace_v = undefined; + dep.set_during_effect = false; } } } @@ -852,28 +847,39 @@ export function get(signal) { } } + recent_async_deriveds.delete(signal); + if ( tracing_mode_flag && + !untracking && tracing_expressions !== null && active_reaction !== null && tracing_expressions.reaction === active_reaction ) { // Used when mapping state between special blocks like `each` - if (signal.debug) { - signal.debug(); - } else if (signal.created) { - var entry = tracing_expressions.entries.get(signal); - - if (entry === undefined) { - entry = { read: [] }; - tracing_expressions.entries.set(signal, entry); - } + if (signal.trace) { + signal.trace(); + } else { + var trace = get_stack('TracedAt'); + + if (trace) { + var entry = tracing_expressions.entries.get(signal); - entry.read.push(get_stack('TracedAt')); + if (entry === undefined) { + entry = { traces: [] }; + tracing_expressions.entries.set(signal, entry); + } + + var last = entry.traces[entry.traces.length - 1]; + + // traces can be duplicated, e.g. by `snapshot` invoking both + // both `getOwnPropertyDescriptor` and `get` traps at once + if (trace.stack !== last?.stack) { + entry.traces.push(trace); + } + } } } - - recent_async_deriveds.delete(signal); } if (is_destroying_effect && old_values.has(signal)) { diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 1c8e5f15c0..0f3bc6fb9f 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 31cddf8f30..09bd06c325 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 63deca62ea..491ffb45cb 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 721673bc36..4176f0ceec 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 3ae8fe5ad1..eed163dbf2 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 4a0b4dfdb3..fd22014cb3 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 c1a8275f15..c77ff9c822 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 879006f057..56732a0402 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 8c50a5c440..d4fcf29e4e 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/src/version.js b/packages/svelte/src/version.js index 2539247e01..42f59dffad 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.33.18'; +export const VERSION = '5.34.0'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 9e094044f7..591851e692 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -193,3 +193,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-browser/samples/css-props-dynamic-component/A.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte new file mode 100644 index 0000000000..694b26f231 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte @@ -0,0 +1,7 @@ +
a
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte new file mode 100644 index 0000000000..06f28c4f75 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte @@ -0,0 +1,7 @@ +
b
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js new file mode 100644 index 0000000000..7629633835 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../assert'; +import { flushSync } from 'svelte'; + +export default test({ + warnings: [], + async test({ assert, target }) { + const btn = target.querySelector('button'); + let div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + flushSync(() => { + btn?.click(); + }); + div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte new file mode 100644 index 0000000000..055ce57da5 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 49f3a919e5..23759d025a 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -3,7 +3,7 @@ import { setImmediate } from 'node:timers/promises'; import { globSync } from 'tinyglobby'; import { createClassComponent } from 'svelte/legacy'; import { proxy } from 'svelte/internal/client'; -import { flushSync, hydrate, mount, unmount } from 'svelte'; +import { flushSync, hydrate, mount, unmount, untrack } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; import { compile_directory, fragments } from '../helpers.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte new file mode 100644 index 0000000000..a22f006dcc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js new file mode 100644 index 0000000000..94cd9d8aaf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, logs }) { + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'array', highlighted: false }, + { log: [{ id: 1, hi: true }] }, + // this _doesn't_ appear in the browser, but it does appear during tests + // and i cannot for the life of me figure out why. this does at least + // test that we don't log `array[0].id` etc + { log: '$state', highlighted: true }, + { log: 'array[0]', highlighted: false }, + { log: { id: 1, hi: true } } + ]); + + logs.length = 0; + + const button = target.querySelector('button'); + button?.click(); + flushSync(); + + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'array', highlighted: false }, + { log: [{ id: 1, hi: false }] }, + { log: '$state', highlighted: false }, + { log: 'array[0]', highlighted: false }, + { log: { id: 1, hi: false } } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte new file mode 100644 index 0000000000..e89ee7d9bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte @@ -0,0 +1,11 @@ + + + + +{#each array as entry (entry.id)} + +{/each} 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 f54f78f5c1..5957d2cc6a 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 c9a66289a1..b4f2cf3691 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: 1 }, - { log: 'effect', highlighted: false }, + { log: 'count', highlighted: false }, + { log: 2 }, + { log: 'effect' }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, - { log: 2 }, - { log: 'effect', highlighted: false }, + { log: 'count', highlighted: false }, + { log: 3 }, + { 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 efa5985e4e..8e9204c90f 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 } ]); }