From cc8d2f6c80cdc9bf0f93c60771e153a3e6ddf0bc Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 10 Jun 2025 17:46:53 +0200 Subject: [PATCH 001/146] docs: add note on `Object.create` (#16124) --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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([ From 17629a60dbaccb49bceb4fdc6fea7a735caf83d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:47:52 -0400 Subject: [PATCH 002/146] Version Packages (#16121) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/flat-points-kick.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/flat-points-kick.md 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/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 506f04c1c4..e91407ddf8 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 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/package.json b/packages/svelte/package.json index 0758158a40..7dd4b08f3d 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.33.19", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2539247e01..2c712391af 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.33.19'; export const PUBLIC_VERSION = '5'; From 91272d702b9cafc310a0176e1e489bb487872fd9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:23:42 -0700 Subject: [PATCH 003/146] feat: add source name logging to `$inspect.trace` (#16060) * init * improve symbol logging * doh * remove proxy path name when reassigned to a source * try this * fix * oops * fix * "unown" proxy when in another source declaration * fix * tag proxy version * proxy bindable props * tag iterables used in destructuring * add changeset, fix failing tests * add comments, minor tweak * lint * somehow forgot to add support for class fields * more class fields * tag_source -> tag, since it applies to deriveds as well * private class fields * this condition is impossible * explicit type narrowing lets us avoid coercion * simplify * unused * tweak * oops, never meant to commit that * minor tweaks * fix private field tagging, only get `declaration` once * fix state declarations in constructors * fix * tag `svelte/reactivity`, `svelte/motion` sources in DEV * try fixing lint * fix intellisense formatting * actually fix lint * replace tag_if_necessary with conditional tagging * avoid [[object Object]] in labels * remove PROXY_REMOVE_PATH * simplify a bit * simplify * tweak * tweak implementation * tweak implementation * tweak implementation * hoist * tweak * fix * WIP (reduce number of with_parent calls, move towards possibility of combining tag and tag_proxy) * DRY out * tweak labels * remove PROXY_REMOVE_PATH (#16126) * remove PROXY_REMOVE_PATH * simplify a bit * simplify * tweak * tweak implementation * tweak implementation * tweak implementation * hoist * tweak * fix * WIP (reduce number of with_parent calls, move towards possibility of combining tag and tag_proxy) * DRY out * come on this was just lazy * fix tests --------- Co-authored-by: Rich Harris --- .changeset/big-teachers-agree.md | 5 + .../client/visitors/AssignmentExpression.js | 19 ++- .../3-transform/client/visitors/ClassBody.js | 29 +++-- .../client/visitors/VariableDeclaration.js | 57 +++++++-- .../svelte/src/internal/client/constants.js | 1 + .../svelte/src/internal/client/dev/tracing.js | 41 +++++- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/client/proxy.js | 119 ++++++++++++++---- .../src/internal/client/reactivity/sources.js | 6 +- .../src/internal/client/reactivity/types.d.ts | 1 + packages/svelte/src/motion/spring.js | 17 ++- packages/svelte/src/motion/tweened.js | 14 ++- .../src/reactivity/create-subscriber.js | 6 + packages/svelte/src/reactivity/date.js | 7 ++ packages/svelte/src/reactivity/map.js | 38 +++++- packages/svelte/src/reactivity/set.js | 15 ++- .../src/reactivity/url-search-params.js | 4 +- packages/svelte/src/reactivity/url.js | 13 ++ .../svelte/src/reactivity/window/index.js | 7 +- packages/svelte/tests/helpers.js | 41 ++++++ .../samples/inspect-trace-nested/_config.js | 35 ++---- .../inspect-trace-reassignment/_config.js | 40 ++---- .../samples/inspect-trace/_config.js | 44 +++---- 23 files changed, 409 insertions(+), 152 deletions(-) create mode 100644 .changeset/big-teachers-agree.md diff --git a/.changeset/big-teachers-agree.md b/.changeset/big-teachers-agree.md new file mode 100644 index 0000000000..f9044541d7 --- /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 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/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index b8d692698d..53f18d42e4 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 7e5196c606..98cef658bf 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -25,3 +25,4 @@ export const EFFECT_IS_UPDATING = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); +export const PROXY_PATH_SYMBOL = Symbol('proxy path'); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index ad80e75c3d..18b99c31b0 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 71c06d7b1b..60f9af9120 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 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 9d2ad2baee..ad7566f772 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 5ef0097649..c445ade846 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 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/tests/helpers.js b/packages/svelte/tests/helpers.js index e62b662372..2d825dbb7c 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 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..683f55f321 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 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 } ]); } From 90807ca18d3f488e606bafa0c585c462056c1280 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 11 Jun 2025 17:17:24 +0200 Subject: [PATCH 004/146] fix: properly hydrate dynamic css props components and remove element removal (#16118) --- .changeset/twelve-foxes-smell.md | 5 ++++ .../3-transform/client/visitors/Component.js | 29 +++++-------------- .../client/visitors/shared/component.js | 17 +++++++---- .../internal/client/dom/blocks/css-props.js | 4 --- .../css-props-dynamic-component/A.svelte | 7 +++++ .../css-props-dynamic-component/B.svelte | 7 +++++ .../css-props-dynamic-component/_config.js | 16 ++++++++++ .../css-props-dynamic-component/main.svelte | 11 +++++++ 8 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 .changeset/twelve-foxes-smell.md create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte diff --git a/.changeset/twelve-foxes-smell.md b/.changeset/twelve-foxes-smell.md new file mode 100644 index 0000000000..1f3be888d8 --- /dev/null +++ b/.changeset/twelve-foxes-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly hydrate dynamic css props components and remove element removal 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/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index ff98d6d378..a1c4025d60 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 @@ -13,10 +13,13 @@ import { determine_slot } from '../../../../../utils/slot.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>} */ @@ -411,7 +414,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, @@ -429,14 +432,18 @@ export function build_component(node, component_name, context, anchor = context. const statements = [...snippet_declarations]; - 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/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/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 From 292af8d38a53295b60ba66eb41a89edd2b1cabec Mon Sep 17 00:00:00 2001 From: "L." Date: Wed, 11 Jun 2025 23:53:14 +0800 Subject: [PATCH 005/146] feat: add `command` and `commandfor` to `HTMLButtonAttributes` (#16117) * feat: add `command` and `commandfor` to `HTMLButtonAttributes` * Update .changeset/empty-cherries-act.md --------- Co-authored-by: Rich Harris --- .changeset/empty-cherries-act.md | 5 +++++ packages/svelte/elements.d.ts | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 .changeset/empty-cherries-act.md diff --git a/.changeset/empty-cherries-act.md b/.changeset/empty-cherries-act.md new file mode 100644 index 0000000000..62758f6e81 --- /dev/null +++ b/.changeset/empty-cherries-act.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `command` and `commandfor` to `HTMLButtonAttributes` 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 { From 438349eb88335fce7f199aac0b95ef05bfb7f453 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Jun 2025 18:44:22 -0400 Subject: [PATCH 006/146] fix: better `$inspect.trace()` output (#16131) * remove code thatcan't be reached and would error if it could * tidy up types, fix duplication * this seems needless * better naming * tidy up * reorder * this code doesn't appear to do anything useful, and no tests fail without it * unused * WIP * revert part of #14811. it makes no sense to show the initial value, it just makes things inconsistent with deriveds. personally i find it more confusing anyway * explanatory comment on both sides * make things a bit more self-explanatory * simplify * missing type * only log UpdatedAt for dirty signals * changeset * lint * Revert "unused" This reverts commit a95b6258004716f4838f05249b7fe718f0ccec2b. * complete revert * ok it works now --- .changeset/strong-clouds-switch.md | 5 ++ .../svelte/src/internal/client/dev/tracing.js | 74 +++++++------------ .../src/internal/client/dom/blocks/each.js | 2 +- .../src/internal/client/reactivity/sources.js | 12 ++- .../src/internal/client/reactivity/types.d.ts | 18 +++-- .../svelte/src/internal/client/runtime.js | 50 +++++++------ .../svelte/tests/runtime-legacy/shared.ts | 2 +- .../samples/inspect-trace-each/Entry.svelte | 8 ++ .../samples/inspect-trace-each/_config.js | 40 ++++++++++ .../samples/inspect-trace-each/main.svelte | 11 +++ .../inspect-trace-reassignment/_config.js | 4 +- 11 files changed, 143 insertions(+), 83 deletions(-) create mode 100644 .changeset/strong-clouds-switch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte diff --git a/.changeset/strong-clouds-switch.md b/.changeset/strong-clouds-switch.md new file mode 100644 index 0000000000..9444d3f164 --- /dev/null +++ b/.changeset/strong-clouds-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better `$inspect.trace()` output diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 18b99c31b0..5834f5bffd 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -6,40 +6,26 @@ 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; @@ -69,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); } } @@ -94,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 }; @@ -101,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; diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 2997664fa2..954dcb2214 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -527,7 +527,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/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index ad7566f772..40a3e4e77f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -37,6 +37,8 @@ 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(); /** @@ -66,7 +68,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; @@ -168,9 +172,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 c445ade846..756bb98f09 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -16,13 +16,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; - label?: string; - 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 51402ac88c..9544060959 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,6 +39,7 @@ import { set_dev_current_component_function } from './context.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; +import { snapshot } from '../shared/clone.js'; let is_flushing = false; @@ -447,19 +448,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; } } } @@ -775,22 +770,33 @@ export function get(signal) { if ( DEV && 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'); - entry.read.push(get_stack('TracedAt')); + if (trace) { + var entry = tracing_expressions.entries.get(signal); + + 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); + } + } } } diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index c0d1177a82..11ea9f6dda 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-reassignment/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js index 683f55f321..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 @@ -36,14 +36,14 @@ export default test({ { log: true }, { log: '$state', highlighted: true }, { log: 'count', highlighted: false }, - { log: 1 }, + { log: 2 }, { log: 'effect' }, { log: '$state', highlighted: false }, { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, { log: 'count', highlighted: false }, - { log: 2 }, + { log: 3 }, { log: 'effect' }, { log: '$state', highlighted: false }, { log: 'checked', highlighted: false }, From ee25c15a9605176f60665ade76e4061fbbea4efd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:03:55 -0400 Subject: [PATCH 007/146] Version Packages (#16128) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-teachers-agree.md | 5 ----- .changeset/empty-cherries-act.md | 5 ----- .changeset/strong-clouds-switch.md | 5 ----- .changeset/twelve-foxes-smell.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 .changeset/big-teachers-agree.md delete mode 100644 .changeset/empty-cherries-act.md delete mode 100644 .changeset/strong-clouds-switch.md delete mode 100644 .changeset/twelve-foxes-smell.md diff --git a/.changeset/big-teachers-agree.md b/.changeset/big-teachers-agree.md deleted file mode 100644 index f9044541d7..0000000000 --- a/.changeset/big-teachers-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add source name logging to `$inspect.trace` diff --git a/.changeset/empty-cherries-act.md b/.changeset/empty-cherries-act.md deleted file mode 100644 index 62758f6e81..0000000000 --- a/.changeset/empty-cherries-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add `command` and `commandfor` to `HTMLButtonAttributes` diff --git a/.changeset/strong-clouds-switch.md b/.changeset/strong-clouds-switch.md deleted file mode 100644 index 9444d3f164..0000000000 --- a/.changeset/strong-clouds-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better `$inspect.trace()` output diff --git a/.changeset/twelve-foxes-smell.md b/.changeset/twelve-foxes-smell.md deleted file mode 100644 index 1f3be888d8..0000000000 --- a/.changeset/twelve-foxes-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: properly hydrate dynamic css props components and remove element removal diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e91407ddf8..34e1c2fc6e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7dd4b08f3d..e412a5a741 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.19", + "version": "5.34.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2c712391af..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.19'; +export const VERSION = '5.34.0'; export const PUBLIC_VERSION = '5'; From e8a7c426d4f80afca31821997ef412824831328b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Jun 2025 19:38:10 -0400 Subject: [PATCH 008/146] remove errant log --- .../runtime-runes/samples/flush-sync-no-scheduled/_config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js index 33a59dfbd3..85e06fa8ec 100644 --- a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js @@ -5,7 +5,6 @@ export default test({ const btn = target.querySelector('button'); const main = target.querySelector('main'); ok(main); - console.log(main.innerHTML); assert.htmlEqual(main.innerHTML, `
true
`); // we don't want to use flush sync (or tick that use it inside) since we are testing that calling `flushSync` once // when there are no scheduled effects does not cause reactivity to break From f5a020d56bdf07cba501895a496c036172a6d995 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Jun 2025 20:26:45 -0400 Subject: [PATCH 009/146] fix: correctly tag private class state fields (#16132) --- .changeset/three-steaks-wash.md | 5 +++ .../3-transform/client/visitors/ClassBody.js | 2 +- .../samples/inspect-trace-class/_config.js | 31 +++++++++++++++++++ .../samples/inspect-trace-class/main.svelte | 28 +++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changeset/three-steaks-wash.md create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte diff --git a/.changeset/three-steaks-wash.md b/.changeset/three-steaks-wash.md new file mode 100644 index 0000000000..caf0ea2bfe --- /dev/null +++ b/.changeset/three-steaks-wash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly tag private class state fields 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 e78a8824dd..5bd9add2a5 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 @@ -78,7 +78,7 @@ export function ClassBody(node, context) { ? /** @type {CallExpression} */ (context.visit(definition.value, child_state)) : undefined; - if (dev) { + if (dev && field.node === definition) { value = b.call('$.tag', value, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`)); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js new file mode 100644 index 0000000000..98b3fb6cbc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js @@ -0,0 +1,31 @@ +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: 'Counter.#count', highlighted: false }, + { log: 0 } + ]); + + logs.length = 0; + + const button = target.querySelector('button'); + button?.click(); + flushSync(); + + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'Counter.#count', highlighted: false }, + { log: 1 } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte new file mode 100644 index 0000000000..56bd497e09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte @@ -0,0 +1,28 @@ + + + From f0497b15ee48b477f5213faff335a68e960e8f7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:55:51 -0400 Subject: [PATCH 010/146] Version Packages (#16133) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/three-steaks-wash.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/three-steaks-wash.md diff --git a/.changeset/three-steaks-wash.md b/.changeset/three-steaks-wash.md deleted file mode 100644 index caf0ea2bfe..0000000000 --- a/.changeset/three-steaks-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly tag private class state fields diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 34e1c2fc6e..23d834108a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.34.1 + +### Patch Changes + +- fix: correctly tag private class state fields ([#16132](https://github.com/sveltejs/svelte/pull/16132)) + ## 5.34.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e412a5a741..83d5b8c3e6 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.34.0", + "version": "5.34.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 42f59dffad..2d603694d2 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.34.0'; +export const VERSION = '5.34.1'; export const PUBLIC_VERSION = '5'; From 6a7df1cce82e2f7df21f056d89458d2fb47530cc Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:59:20 +0800 Subject: [PATCH 011/146] fix: add missing typings for some dimension bindings (#16142) * fix: typings for dimension bindings * tweak description --- .changeset/clever-cats-invent.md | 5 +++++ packages/svelte/elements.d.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/clever-cats-invent.md diff --git a/.changeset/clever-cats-invent.md b/.changeset/clever-cats-invent.md new file mode 100644 index 0000000000..589b7d3eae --- /dev/null +++ b/.changeset/clever-cats-invent.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add missing typings for some dimension bindings diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 076cc71b38..fe4078f56a 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -844,6 +844,10 @@ export interface HTMLAttributes extends AriaAttributes, D readonly 'bind:borderBoxSize'?: Array | undefined | null; readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null; readonly 'bind:focused'?: boolean | undefined | null; + readonly 'bind:clientWidth'?: number | undefined | null; + readonly 'bind:clientHeight'?: number | undefined | null; + readonly 'bind:offsetWidth'?: number | undefined | null; + readonly 'bind:offsetHeight'?: number | undefined | null; // SvelteKit 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; From 36315fece462cf1a2afc4f1d44f9f527df3ae7db Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 14 Jun 2025 04:18:52 -0700 Subject: [PATCH 012/146] fix: prune typescript class field declarations (#16154) * fix: prune typescript class field declarations * add test --- .changeset/neat-lemons-occur.md | 5 +++++ .../phases/1-parse/remove_typescript_nodes.js | 13 +++++++++++++ .../runtime-runes/samples/typescript/main.svelte | 1 + 3 files changed, 19 insertions(+) create mode 100644 .changeset/neat-lemons-occur.md diff --git a/.changeset/neat-lemons-occur.md b/.changeset/neat-lemons-occur.md new file mode 100644 index 0000000000..53d77ed764 --- /dev/null +++ b/.changeset/neat-lemons-occur.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prune typescript class field declarations diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index aba94ee20d..cb498c3c13 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -115,6 +115,19 @@ const visitors = { TSDeclareFunction() { return b.empty; }, + ClassBody(node, context) { + const body = []; + for (const _child of node.body) { + const child = context.visit(_child); + if (child.type !== 'PropertyDefinition' || !child.declare) { + body.push(child); + } + } + return { + ...node, + body + }; + }, ClassDeclaration(node, context) { if (node.declare) { return b.empty; diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index d1b6452df4..4fc7c4ec38 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -14,6 +14,7 @@ class Foo { public name: string; + declare bar: string; x = 'x' as const; constructor(name: string) { this.name = name; From da63318191f85baf302e494615f23b46f74908f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 08:31:00 -0400 Subject: [PATCH 013/146] Version Packages (#16147) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-cats-invent.md | 5 ----- .changeset/neat-lemons-occur.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/clever-cats-invent.md delete mode 100644 .changeset/neat-lemons-occur.md diff --git a/.changeset/clever-cats-invent.md b/.changeset/clever-cats-invent.md deleted file mode 100644 index 589b7d3eae..0000000000 --- a/.changeset/clever-cats-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add missing typings for some dimension bindings diff --git a/.changeset/neat-lemons-occur.md b/.changeset/neat-lemons-occur.md deleted file mode 100644 index 53d77ed764..0000000000 --- a/.changeset/neat-lemons-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prune typescript class field declarations diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 23d834108a..563086d8d9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.34.2 + +### Patch Changes + +- fix: add missing typings for some dimension bindings ([#16142](https://github.com/sveltejs/svelte/pull/16142)) + +- fix: prune typescript class field declarations ([#16154](https://github.com/sveltejs/svelte/pull/16154)) + ## 5.34.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 83d5b8c3e6..2e4234fcaf 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.34.1", + "version": "5.34.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2d603694d2..c1df14d295 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.34.1'; +export const VERSION = '5.34.2'; export const PUBLIC_VERSION = '5'; From 113a3daab2a2742127a5857b76d1314deb260e03 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 14 Jun 2025 17:14:39 +0200 Subject: [PATCH 014/146] fix: don't define `error.message` if it's not configurable (#16149) * fix: don't define `error.message` if it's not configurable * fix: print console.error with updated stack * fix: revert second `console.error` --- .changeset/unlucky-plants-jump.md | 5 +++++ packages/svelte/src/internal/client/error-handling.js | 8 +++++++- .../samples/non-configurable-errors/_config.js | 8 ++++++++ .../samples/non-configurable-errors/main.svelte | 11 +++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/unlucky-plants-jump.md create mode 100644 packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte diff --git a/.changeset/unlucky-plants-jump.md b/.changeset/unlucky-plants-jump.md new file mode 100644 index 0000000000..814cb99b69 --- /dev/null +++ b/.changeset/unlucky-plants-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't define `error.message` if it's not configurable diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index aeec1d8b47..99f3ed6cd4 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; -import { define_property } from '../shared/utils.js'; +import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect } from './runtime.js'; /** @@ -63,6 +63,12 @@ function adjust_error(error, effect) { if (adjusted_errors.has(error)) return; adjusted_errors.add(error); + const message_descriptor = get_descriptor(error, 'message'); + + // if the message was already changed and it's not configurable we can't change it + // or it will throw a different error swallowing the original error + if (message_descriptor && !message_descriptor.configurable) return; + var indent = is_firefox ? ' ' : '\t'; var component_stack = `\n${indent}in ${effect.fn?.name || ''}`; var context = effect.ctx; diff --git a/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js new file mode 100644 index 0000000000..5bbe4483d3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + error: 'test' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte new file mode 100644 index 0000000000..f71a5e6c43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte @@ -0,0 +1,11 @@ + From 92ea58bee6f27dde6b1d29527c0e2f97047219f0 Mon Sep 17 00:00:00 2001 From: Matei Trandafir Date: Sat, 14 Jun 2025 18:15:19 +0300 Subject: [PATCH 015/146] fix: don't eagerly execute deriveds on resume (#16150) * Add failing test * Add {@const} test case * Fix the bug * Add yet another test case * Better fix * Changeset * simplify * this appears to be unnecessary --------- Co-authored-by: Rich Harris --- .changeset/poor-pumpkins-exercise.md | 5 ++++ .../src/internal/client/reactivity/effects.js | 13 ++++------- .../if-nested-template/Component.svelte | 7 ++++++ .../samples/if-nested-template/_config.js | 17 ++++++++++++++ .../samples/if-nested-template/main.svelte | 23 +++++++++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 .changeset/poor-pumpkins-exercise.md create mode 100644 packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte diff --git a/.changeset/poor-pumpkins-exercise.md b/.changeset/poor-pumpkins-exercise.md new file mode 100644 index 0000000000..cd5d08a9b5 --- /dev/null +++ b/.changeset/poor-pumpkins-exercise.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: don't eagerly execute deriveds on resume diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 54994b9bd1..625c0d1822 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -329,6 +329,7 @@ export function render_effect(fn) { /** * @param {(...expressions: any) => void | (() => void)} fn * @param {Array<() => any>} thunks + * @param {(fn: () => T) => Derived} d * @returns {Effect} */ export function template_effect(fn, thunks = [], d = derived) { @@ -598,15 +599,11 @@ function resume_children(effect, local) { if ((effect.f & INERT) === 0) return; effect.f ^= INERT; - // Ensure the effect is marked as clean again so that any dirty child - // effects can schedule themselves for execution - if ((effect.f & CLEAN) === 0) { - effect.f ^= CLEAN; - } - // If a dependency of this effect changed while it was paused, - // schedule the effect to update - if (check_dirtiness(effect)) { + // schedule the effect to update. we don't use `check_dirtiness` + // here because we don't want to eagerly recompute a derived like + // `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined + if ((effect.f & CLEAN) !== 0) { set_signal_status(effect, DIRTY); schedule_effect(effect); } diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte b/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte new file mode 100644 index 0000000000..b4281bbcbd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js b/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js new file mode 100644 index 0000000000..673f668916 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1, btn2] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + flushSync(() => btn1?.click()); + assert.htmlEqual(div.innerHTML, '123 123'); + assert.equal(div.inert, true); + + flushSync(() => btn2?.click()); + assert.htmlEqual(div.innerHTML, ''); + assert.deepEqual(logs, ['123']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte b/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte new file mode 100644 index 0000000000..04afa7d664 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte @@ -0,0 +1,23 @@ + + +{#if outer} +
+ {#if inner} + {@const text = inner.toString()} + {text} {inner.toString()} + + {/if} +
+{/if} + + + From 6636f748ae770e905ca9e88e9a921b4bc854164c Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 14 Jun 2025 18:19:42 +0300 Subject: [PATCH 016/146] fix: prevent memory leaking signals in legacy mode (#16145) * fix: prevent memory leaking signals in legacy mode * format * fix --------- Co-authored-by: 7nik --- .changeset/soft-oranges-approve.md | 5 +++++ packages/svelte/src/internal/client/dom/blocks/await.js | 6 ++++-- packages/svelte/src/internal/client/dom/blocks/each.js | 2 +- packages/svelte/src/internal/client/reactivity/sources.js | 4 ++-- packages/svelte/src/legacy/legacy-client.js | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/soft-oranges-approve.md diff --git a/.changeset/soft-oranges-approve.md b/.changeset/soft-oranges-approve.md new file mode 100644 index 0000000000..f759c128c0 --- /dev/null +++ b/.changeset/soft-oranges-approve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent memory leaking signals in legacy mode diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 99bdc0000c..47df5fc9a5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -58,8 +58,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { /** @type {Effect | null} */ var catch_effect; - var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined)); - var error_source = (runes ? source : mutable_source)(undefined); + var input_source = runes + ? source(/** @type {V} */ (undefined)) + : mutable_source(/** @type {V} */ (undefined), false, false); + var error_source = runes ? source(undefined) : mutable_source(undefined, false, false); var resolved = false; /** diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 954dcb2214..b638a6d2da 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -521,7 +521,7 @@ function create_item( var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0; - var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value; + var v = reactive ? (mutable ? mutable_source(value, false, false) : source(value)) : value; var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); if (DEV && reactive) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 40a3e4e77f..56f4138252 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -97,7 +97,7 @@ export function state(v, stack) { * @returns {Source} */ /*#__NO_SIDE_EFFECTS__*/ -export function mutable_source(initial_value, immutable = false) { +export function mutable_source(initial_value, immutable = false, trackable = true) { const s = source(initial_value); if (!immutable) { s.equals = safe_equals; @@ -105,7 +105,7 @@ export function mutable_source(initial_value, immutable = false) { // bind the signal to the component context, in case we need to // track updates to trigger beforeUpdate/afterUpdate callbacks - if (legacy_mode_flag && component_context !== null && component_context.l !== null) { + if (legacy_mode_flag && trackable && component_context !== null && component_context.l !== null) { (component_context.l.s ??= []).push(s); } diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index bb9a5a9c03..45c478ecab 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -82,7 +82,7 @@ class Svelte4Component { * @param {unknown} value */ var add_source = (key, value) => { - var s = mutable_source(value); + var s = mutable_source(value, false, false); sources.set(key, s); return s; }; From 5c5536eaa158b4404f49ede3401e7cdee99be165 Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 14 Jun 2025 18:48:32 +0300 Subject: [PATCH 017/146] docs: clarify style precedence (#16158) Co-authored-by: 7nik --- documentation/docs/03-template-syntax/17-style.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index 749376c6e2..aa61cdcde3 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -34,8 +34,10 @@ To mark a style as important, use the `|important` modifier:
...
``` -When `style:` directives are combined with `style` attributes, the directives will take precedence: +When `style:` directives are combined with `style` attributes, the directives will take precedence, +even over `!important` properties: ```svelte -
This will be red
+
This will be red
+
This will still be red
``` From 546608636a0018a78c3441adb60246c49fa86468 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:48:47 -0400 Subject: [PATCH 018/146] Version Packages (#16157) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/poor-pumpkins-exercise.md | 5 ----- .changeset/soft-oranges-approve.md | 5 ----- .changeset/unlucky-plants-jump.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/poor-pumpkins-exercise.md delete mode 100644 .changeset/soft-oranges-approve.md delete mode 100644 .changeset/unlucky-plants-jump.md diff --git a/.changeset/poor-pumpkins-exercise.md b/.changeset/poor-pumpkins-exercise.md deleted file mode 100644 index cd5d08a9b5..0000000000 --- a/.changeset/poor-pumpkins-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: don't eagerly execute deriveds on resume diff --git a/.changeset/soft-oranges-approve.md b/.changeset/soft-oranges-approve.md deleted file mode 100644 index f759c128c0..0000000000 --- a/.changeset/soft-oranges-approve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent memory leaking signals in legacy mode diff --git a/.changeset/unlucky-plants-jump.md b/.changeset/unlucky-plants-jump.md deleted file mode 100644 index 814cb99b69..0000000000 --- a/.changeset/unlucky-plants-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't define `error.message` if it's not configurable diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 563086d8d9..020942f5fd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.34.3 + +### Patch Changes + +- fix: don't eagerly execute deriveds on resume ([#16150](https://github.com/sveltejs/svelte/pull/16150)) + +- fix: prevent memory leaking signals in legacy mode ([#16145](https://github.com/sveltejs/svelte/pull/16145)) + +- fix: don't define `error.message` if it's not configurable ([#16149](https://github.com/sveltejs/svelte/pull/16149)) + ## 5.34.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2e4234fcaf..d2fbdb32f7 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.34.2", + "version": "5.34.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c1df14d295..01888eaa78 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.34.2'; +export const VERSION = '5.34.3'; export const PUBLIC_VERSION = '5'; From c91e69b15b4c36c245f1777e19570cb3337f7ad2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 15 Jun 2025 15:35:21 -0400 Subject: [PATCH 019/146] chore: remove some unnecessary code (#16160) --- .../svelte/src/internal/client/reactivity/effects.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 625c0d1822..03d073781d 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -599,15 +599,6 @@ function resume_children(effect, local) { if ((effect.f & INERT) === 0) return; effect.f ^= INERT; - // If a dependency of this effect changed while it was paused, - // schedule the effect to update. we don't use `check_dirtiness` - // here because we don't want to eagerly recompute a derived like - // `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined - if ((effect.f & CLEAN) !== 0) { - set_signal_status(effect, DIRTY); - schedule_effect(effect); - } - var child = effect.first; while (child !== null) { From 931f211b253e13d7bb8fe6db441cdc3800da5f3a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 15 Jun 2025 15:40:31 -0400 Subject: [PATCH 020/146] chore: fix `$.attachment` prop (#16172) --- .../phases/3-transform/client/visitors/shared/component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a1c4025d60..19ed9cdeb8 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 @@ -275,7 +275,7 @@ export function build_component(node, component_name, context) { ); } - push_prop(b.prop('get', b.call('$.attachment'), expression, true)); + push_prop(b.prop('init', b.call('$.attachment'), expression, true)); } } From 7e588857c2b92a98ebc25237112e29f6f8988992 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 16 Jun 2025 17:49:43 +0200 Subject: [PATCH 021/146] fix: don't set state withing `with_parent` in proxy (#16176) Closes #16164 We can't set everywhere within with_parent otherwise if it's the first time we are reading a derived it could look like we are setting state in a derived (which you are not). --- .changeset/beige-plants-laugh.md | 5 ++++ packages/svelte/src/internal/client/proxy.js | 27 ++++++++----------- .../samples/proxy-set-with-parent/_config.js | 5 ++++ .../samples/proxy-set-with-parent/main.svelte | 15 +++++++++++ 4 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 .changeset/beige-plants-laugh.md create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte diff --git a/.changeset/beige-plants-laugh.md b/.changeset/beige-plants-laugh.md new file mode 100644 index 0000000000..d0c771736a --- /dev/null +++ b/.changeset/beige-plants-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't set state withing `with_parent` in proxy diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4870506699..60eba6aa87 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -93,21 +93,19 @@ export function proxy(value) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants e.state_descriptors_fixed(); } - - with_parent(() => { - var s = sources.get(prop); - - if (s === undefined) { - s = source(descriptor.value, stack); + var s = sources.get(prop); + if (s === undefined) { + s = with_parent(() => { + var 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 s; + }); + } else { + set(s, descriptor.value, true); + } return true; }, @@ -268,11 +266,8 @@ export function proxy(value) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = with_parent(() => { - var s = source(undefined, stack); - set(s, proxy(value)); - return s; - }); + s = with_parent(() => source(undefined, stack)); + set(s, proxy(value)); sources.set(prop, s); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte new file mode 100644 index 0000000000..7450eff3fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte @@ -0,0 +1,15 @@ + + + From d99d87251905abd57404f46f16df4d299ddf9d3b Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Mon, 16 Jun 2025 23:54:12 -0600 Subject: [PATCH 022/146] fix: Make docs example not infinitely recurse (#16183) --- documentation/docs/07-misc/02-testing.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 64bf49d77a..db99b70770 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -129,12 +129,12 @@ test('Effect', () => { // effects normally run after a microtask, // use flushSync to execute all pending effects synchronously flushSync(); - expect(log.value).toEqual([0]); + expect(log).toEqual([0]); count = 1; flushSync(); - expect(log.value).toEqual([0, 1]); + expect(log).toEqual([0, 1]); }); cleanup(); @@ -148,17 +148,13 @@ test('Effect', () => { */ export function logger(getValue) { /** @type {any[]} */ - let log = $state([]); + let log = []; $effect(() => { log.push(getValue()); }); - return { - get value() { - return log; - } - }; + return log; } ``` From b224c3fb4b019880a20f4286e60f078e1dcbacc0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 12:43:52 -0400 Subject: [PATCH 023/146] fix: coarse reactivity, alternative approach (#16100) Make sure we track statically visible dependencies and untrack indirect dependencies Fixes #14351 --------- Co-authored-by: 7nik Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/popular-dancers-switch.md | 5 ++ .../src/compiler/phases/1-parse/state/tag.js | 29 +++++++-- .../visitors/AssignmentExpression.js | 4 ++ .../phases/2-analyze/visitors/AwaitBlock.js | 5 +- .../phases/2-analyze/visitors/ConstTag.js | 5 +- .../phases/2-analyze/visitors/HtmlTag.js | 2 +- .../phases/2-analyze/visitors/Identifier.js | 1 + .../phases/2-analyze/visitors/IfBlock.js | 8 ++- .../phases/2-analyze/visitors/KeyBlock.js | 3 +- .../2-analyze/visitors/MemberExpression.js | 5 +- .../phases/2-analyze/visitors/RenderTag.js | 2 +- .../2-analyze/visitors/UpdateExpression.js | 4 ++ .../2-analyze/visitors/shared/function.js | 10 ++++ .../3-transform/client/visitors/AttachTag.js | 13 +--- .../3-transform/client/visitors/AwaitBlock.js | 5 +- .../3-transform/client/visitors/ConstTag.js | 24 ++++---- .../3-transform/client/visitors/EachBlock.js | 21 ++++--- .../3-transform/client/visitors/HtmlTag.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 5 +- .../3-transform/client/visitors/KeyBlock.js | 3 +- .../client/visitors/RegularElement.js | 2 +- .../3-transform/client/visitors/RenderTag.js | 12 +++- .../client/visitors/TitleElement.js | 3 +- .../client/visitors/shared/element.js | 6 +- .../client/visitors/shared/fragment.js | 28 ++++----- .../client/visitors/shared/utils.js | 59 ++++++++++++++++--- packages/svelte/src/compiler/phases/nodes.js | 5 +- packages/svelte/src/compiler/types/index.d.ts | 8 ++- .../svelte/src/compiler/types/template.d.ts | 21 +++++++ .../block-expression-assign/_config.js | 12 ++++ .../block-expression-assign/main.svelte | 45 ++++++++++++++ .../block-expression-fn-call/_config.js | 12 ++++ .../block-expression-fn-call/main.svelte | 36 +++++++++++ .../block-expression-member-access/_config.js | 12 ++++ .../main.svelte | 46 +++++++++++++++ .../Item.svelte | 4 +- .../main.svelte | 4 +- .../purity/_expected/client/index.svelte.js | 6 +- 38 files changed, 392 insertions(+), 87 deletions(-) create mode 100644 .changeset/popular-dancers-switch.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte diff --git a/.changeset/popular-dancers-switch.md b/.changeset/popular-dancers-switch.md new file mode 100644 index 0000000000..b8c26c210e --- /dev/null +++ b/.changeset/popular-dancers-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use compiler-driven reactivity in legacy mode template expressions diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 4153463c83..5d77d6a8f4 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -63,7 +63,10 @@ function open(parser) { end: -1, test: read_expression(parser), consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.allow_whitespace(); @@ -244,7 +247,10 @@ function open(parser) { error: null, pending: null, then: null, - catch: null + catch: null, + metadata: { + expression: create_expression_metadata() + } }); if (parser.eat('then')) { @@ -326,7 +332,10 @@ function open(parser) { start, end: -1, expression, - fragment: create_fragment() + fragment: create_fragment(), + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(block); @@ -461,7 +470,10 @@ function next(parser) { elseif: true, test: expression, consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(child); @@ -624,7 +636,10 @@ function special(parser) { type: 'HtmlTag', start, end: parser.index, - expression + expression, + metadata: { + expression: create_expression_metadata() + } }); return; @@ -699,6 +714,9 @@ function special(parser) { declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], start: start + 2, // start at const, not at @const end: parser.index - 1 + }, + metadata: { + expression: create_expression_metadata() } }); } @@ -725,6 +743,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { + expression: create_expression_metadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index 673c79f2df..39358f72fc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index a71f325154..5aa04ba3b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,5 +41,8 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index f723f8447c..d5f5f7b2e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -32,5 +32,8 @@ export function ConstTag(node, context) { e.const_tag_invalid_placement(node); } - context.next(); + const declaration = node.declaration.declarations[0]; + + context.visit(declaration.id); + context.visit(declaration.init, { ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js index c89b11ad36..7b0e501760 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,5 +15,5 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next(); + context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index abf70769c0..cced326f9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -90,6 +90,7 @@ export function Identifier(node, context) { if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); + context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && !binding.is_function() && diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index a65771bcfc..dcdae3587f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -17,5 +17,11 @@ export function IfBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.test, { + ...context.state, + expression: node.metadata.expression + }); + + context.visit(node.consequent); + if (node.alternate) context.visit(node.alternate); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index 88bb6a98e7..09e604ea66 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,5 +16,6 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 245a164c71..0a3b386198 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -15,8 +15,9 @@ export function MemberExpression(node, context) { } } - if (context.state.expression && !is_pure(node, context)) { - context.state.expression.has_state = true; + if (context.state.expression) { + context.state.expression.has_member_expression = true; + context.state.expression.has_state ||= !is_pure(node, context); } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index a8c9d408bd..1230ef6b04 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -54,7 +54,7 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.visit(callee); + context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { const metadata = create_expression_metadata(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 13f4b9019e..ed48e026ac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -21,5 +21,9 @@ export function UpdateExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index c892efd421..1776167850 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -13,6 +13,16 @@ export function visit_function(node, context) { scope: context.state.scope }; + if (context.state.expression) { + for (const [name] of context.state.scope.references) { + const binding = context.state.scope.get(name); + + if (binding && binding.scope.function_depth < context.state.scope.function_depth) { + context.state.expression.references.add(binding); + } + } + } + context.next({ ...context.state, function_depth: context.state.function_depth + 1, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 062604cacc..8b1570c7dc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -1,21 +1,14 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AttachTag} node * @param {ComponentContext} context */ export function AttachTag(node, context) { - context.state.init.push( - b.stmt( - b.call( - '$.attach', - context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))) - ) - ) - ); + const expression = build_expression(context, node.expression, node.metadata.expression); + context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 30e370327f..7873cf3ddb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,10 +1,11 @@ -/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ +/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -14,7 +15,7 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); + const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 2f3c0b3d0e..c1be1e3220 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -1,4 +1,4 @@ -/** @import { Expression, Pattern } from 'estree' */ +/** @import { Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -15,15 +16,8 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - context.state.init.push( - b.const( - declaration.id, - create_derived( - context.state, - b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) - ) - ) - ); + const init = build_expression(context, declaration.init, node.metadata.expression); + context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -48,13 +42,15 @@ export function ConstTag(node, context) { // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object + const init = build_expression( + { ...context, state: child_state }, + declaration.init, + node.metadata.expression + ); const fn = b.arrow( [], b.block([ - b.const( - /** @type {Pattern} */ (context.visit(declaration.id, child_state)), - /** @type {Expression} */ (context.visit(declaration.init, child_state)) - ), + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 6c651464f1..201c4b278f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ @@ -12,8 +12,8 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -24,11 +24,18 @@ export function EachBlock(node, context) { // expression should be evaluated in the parent scope, not the scope // created by the each block itself - const collection = /** @type {Expression} */ ( - context.visit(node.expression, { - ...context.state, - scope: /** @type {Scope} */ (context.state.scope.parent) - }) + const parent_scope_state = { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }; + + const collection = build_expression( + { + ...context, + state: parent_scope_state + }, + node.expression, + node.metadata.expression ); if (!each_node_meta.is_controlled) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 405b400b42..fb59967996 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -1,8 +1,8 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -11,7 +11,7 @@ import * as b from '#compiler/builders'; export function HtmlTag(node, context) { context.state.template.push_comment(); - const expression = /** @type {Expression} */ (context.visit(node.expression)); + const expression = build_expression(context, node.expression, node.metadata.expression); const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 3702a47bc9..deab040e50 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -31,6 +32,8 @@ export function IfBlock(node, context) { statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } + const test = build_expression(context, node.test, node.metadata.expression); + /** @type {Expression[]} */ const args = [ node.elseif ? b.id('$$anchor') : context.state.node, @@ -38,7 +41,7 @@ export function IfBlock(node, context) { [b.id('$$render')], b.block([ b.if( - /** @type {Expression} */ (context.visit(node.test)), + test, b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index 5e63f7e872..2f17479c7e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -10,7 +11,7 @@ import * as b from '#compiler/builders'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = /** @type {Expression} */ (context.visit(node.expression)); + const key = build_expression(context, node.expression, node.metadata.expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index e823792993..1aefff0db0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -331,7 +331,7 @@ export function RegularElement(node, context) { trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { - const { value } = build_template_chunk(trimmed, context.visit, child_state); + const { value } = build_template_chunk(trimmed, context, child_state); const empty_string = value.type === 'Literal' && value.value === ''; if (!empty_string) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index fec7b5762a..c3615d9d50 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -19,7 +20,10 @@ export function RenderTag(node, context) { /** @type {Expression[]} */ let args = []; for (let i = 0; i < raw_args.length; i++) { - let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i]))); + let thunk = b.thunk( + build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i]) + ); + const { has_call } = node.metadata.arguments[i]; if (has_call) { @@ -31,7 +35,11 @@ export function RenderTag(node, context) { } } - let snippet_function = /** @type {Expression} */ (context.visit(callee)); + let snippet_function = build_expression( + context, + /** @type {Expression} */ (callee), + node.metadata.expression + ); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 7bfdaf1850..e6f4202a01 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js'; export function TitleElement(node, context) { const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context.visit, - context.state + context ); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 67de25b770..10f942b7d4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes @@ -121,7 +121,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: b.literal(chunk.data), has_state: false }; } - let expression = /** @type {Expression} */ (context.visit(chunk.expression)); + let expression = build_expression(context, chunk.expression, chunk.metadata.expression); return { value: memoize(expression, chunk.metadata.expression), @@ -129,7 +129,7 @@ export function build_attribute_value(value, context, memoize = (value) => value }; } - return build_template_chunk(value, context.visit, context.state, memoize); + return build_template_chunk(value, context, context.state, memoize); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 7af2c2d4aa..62d07014ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js'; * @param {boolean} is_element * @param {ComponentContext} context */ -export function process_children(nodes, initial, is_element, { visit, state }) { - const within_bound_contenteditable = state.metadata.bound_contenteditable; +export function process_children(nodes, initial, is_element, context) { + const within_bound_contenteditable = context.state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { let id = expression; if (id.type !== 'Identifier') { - id = b.id(state.scope.generate(name)); - state.init.push(b.var(id, expression)); + id = b.id(context.state.scope.generate(name)); + context.state.init.push(b.var(id, expression)); } prev = () => id; @@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) { function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { skipped += 1; - state.template.push_text(sequence); + context.state.template.push_text(sequence); return; } - state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); + context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); - const { has_state, value } = build_template_chunk(sequence, visit, state); + const { has_state, value } = build_template_chunk(sequence, context); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_state && !within_bound_contenteditable) { - state.update.push(update); + context.state.update.push(update); } else { - state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } @@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) { sequence = []; } - let child_state = state; + let child_state = context.state; - if (is_static_element(node, state)) { + if (is_static_element(node, context.state)) { skipped += 1; } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { node.metadata.is_controlled = true; } else { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node'); - child_state = { ...state, node: id }; + child_state = { ...context.state, node: id }; } - visit(node, child_state); + context.visit(node, child_state); } } @@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { // traverse to the last (n - 1) one when hydrating if (skipped > 1) { skipped -= 1; - state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); + context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ebf88e878f..15982899c9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,6 +1,6 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ +/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState, Context } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; -import { create_derived } from '../../utils.js'; +import { build_getter, create_derived } from '../../utils.js'; /** * @param {ComponentClientTransformState} state @@ -31,15 +31,15 @@ export function get_expression_id(expressions, value) { /** * @param {Array} values - * @param {(node: AST.SvelteNode, state: any) => any} visit + * @param {ComponentContext} context * @param {ComponentClientTransformState} state * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_template_chunk( values, - visit, - state, + context, + state = context.state, memoize = (value, metadata) => metadata.has_call ? get_expression_id(state.expressions, value) : value ) { @@ -66,7 +66,7 @@ export function build_template_chunk( state.scope.get('undefined') ) { let value = memoize( - /** @type {Expression} */ (visit(node.expression, state)), + build_expression(context, node.expression, node.metadata.expression, state), node.metadata.expression ); @@ -360,3 +360,48 @@ export function validate_mutation(node, context, expression) { loc && b.literal(loc.column) ); } + +/** + * + * @param {ComponentContext} context + * @param {Expression} expression + * @param {ExpressionMetadata} metadata + */ +export function build_expression(context, expression, metadata, state = context.state) { + const value = /** @type {Expression} */ (context.visit(expression, state)); + + if (context.state.analysis.runes) { + return value; + } + + if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) { + return value; + } + + // Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here + const sequence = b.sequence([]); + + for (const binding of metadata.references) { + if (binding.kind === 'normal' && binding.declaration_kind !== 'import') { + continue; + } + + var getter = build_getter({ ...binding.node }, state); + + if ( + binding.kind === 'bindable_prop' || + binding.kind === 'template' || + binding.declaration_kind === 'import' || + binding.node.name === '$$props' || + binding.node.name === '$$restProps' + ) { + getter = b.call('$.deep_read_state', getter); + } + + sequence.expressions.push(getter); + } + + sequence.expressions.push(b.call('$.untrack', b.thunk(value))); + + return sequence; +} diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 2043747ed0..c35f194b75 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -62,8 +62,11 @@ export function create_attribute(name, start, end, value) { export function create_expression_metadata() { return { dependencies: new Set(), + references: new Set(), has_state: false, - has_call: false + has_call: false, + has_member_expression: false, + has_assignment: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index fdd6024726..558ee558f7 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -279,12 +279,18 @@ export type DeclarationKind = | 'synthetic'; export interface ExpressionMetadata { - /** All the bindings that are referenced inside this expression */ + /** All the bindings that are referenced eagerly (not inside functions) in this expression */ dependencies: Set; + /** All the bindings that are referenced inside this expression, including inside functions */ + references: Set; /** True if the expression references state directly, or _might_ (via member/call expressions) */ has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ has_call: boolean; + /** True if the expression includes a member expression */ + has_member_expression: boolean; + /** True if the expression includes an assignment or an update */ + has_assignment: boolean; } export interface StateField { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index cefc7fa7a2..2a7ec7b5c6 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -135,6 +135,10 @@ export namespace AST { export interface HtmlTag extends BaseNode { type: 'HtmlTag'; expression: Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** An HTML comment */ @@ -151,6 +155,10 @@ export namespace AST { declaration: VariableDeclaration & { declarations: [VariableDeclarator & { id: Pattern; init: Expression }]; }; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `{@debug ...}` tag */ @@ -165,6 +173,7 @@ export namespace AST { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); /** @internal */ metadata: { + expression: ExpressionMetadata; dynamic: boolean; arguments: ExpressionMetadata[]; path: SvelteNode[]; @@ -447,6 +456,10 @@ export namespace AST { test: Expression; consequent: Fragment; alternate: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** An `{#await ...}` block */ @@ -461,12 +474,20 @@ export namespace AST { pending: Fragment | null; then: Fragment | null; catch: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface KeyBlock extends BaseNode { type: 'KeyBlock'; expression: Expression; fragment: Fragment; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface SnippetBlock extends BaseNode { diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js new file mode 100644 index 0000000000..15adef2c9b --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte new file mode 100644 index 0000000000..67190669ed --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte @@ -0,0 +1,45 @@ + + +{#if a = 0}{/if} + +{#each [b = 0] as x}{x,''}{/each} + +{#key c = 0}{/key} + +{#await d = 0}{/await} + +{#snippet snip()}{/snippet} + +{@render (e = 0, snip)()} + +{@html f = 0, ''} + +
+ +{#key 1} + {@const x = (h = 0)} + {x, ''} +{/key} + +{#if 1} + {@const x = (i = 0)} + {x, ''} +{/if} + + +[{a},{b},{c},{d},{e},{f},{g},{h},{i}] + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js new file mode 100644 index 0000000000..523dcd625d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
12 - 12`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
13 - 12`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte new file mode 100644 index 0000000000..37838f091f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte @@ -0,0 +1,36 @@ + + +{#if fn(false)}{:else if fn(true)}{/if} + +{#each fn([]) as x}{x, ''}{/each} + +{#key fn(1)}{/key} + +{#await fn(Promise.resolve())}{/await} + +{#snippet snip()}{/snippet} + +{@render fn(snip)()} + +{@html fn('')} + +
{})}>
+ +{#key 1} + {@const x = fn('')} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js new file mode 100644 index 0000000000..0e1a5a8150 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
10 - 10`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
11 - 10`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte new file mode 100644 index 0000000000..4041be4f6f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte @@ -0,0 +1,46 @@ + + +{#if obj.false}{:else if obj.true}{/if} + +{#each obj.array as x}{x, ''}{/each} + +{#key obj.string}{/key} + +{#await obj.promise}{/await} + +{#snippet snip()}{/snippet} + +{@render obj.snippet()} + +{@html obj.string} + +
+ +{#key 1} + {@const x = obj.string} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte index b2e6cd046c..4127e857d5 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte @@ -5,7 +5,7 @@ export let index; export let n; - function logRender () { + function logRender (n) { order.push(`${index}: render ${n}`); return index; } @@ -24,5 +24,5 @@
  • - {logRender()} + {logRender(n)}
  • diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte index b05b1476fd..51dee3bc0c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte @@ -5,7 +5,7 @@ export let n = 0; - function logRender () { + function logRender (n) { order.push(`parent: render ${n}`); return 'parent'; } @@ -23,7 +23,7 @@ }) -{logRender()} +{logRender(n)}