From 140462374a759d7c38262e9823a7db6c73bcc067 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 13:32:15 -0400 Subject: [PATCH 1/4] fix: re-evaluate derived props during teardown (#16278) * Store deriveds old value before updating them for consistency with directly assigned sources when reading in teardown functions * Add tests for derived old values in teardown functions * Add tests for props old values in onDestroy hooks to prevent regressions * Add changeset * Update comment * remove extraneous tests - these pass with or without the src change * revert * WIP * simplify props * simplify * tweak * reorder a bit * simplify * unused * more * more * tweak * also appears to be unnecessary * changeset * fix * Update .changeset/light-rivers-jump.md * easier debugging * fix * WIP * fix --------- Co-authored-by: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> --- .changeset/light-rivers-jump.md | 5 ++ .../internal/client/reactivity/deriveds.js | 3 +- .../src/internal/client/reactivity/props.js | 80 ++++++++++++++----- .../svelte/src/internal/client/runtime.js | 44 +++++++++- .../derived-cleanup-old-value/_config.js | 22 +++++ .../derived-cleanup-old-value/main.svelte | 20 +++++ .../effect-teardown-derived/Component.svelte | 15 ++++ .../effect-teardown-derived/_config.js | 17 ++++ .../effect-teardown-derived/main.svelte | 13 +++ 9 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 .changeset/light-rivers-jump.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md new file mode 100644 index 000000000000..2454d5715602 --- /dev/null +++ b/.changeset/light-rivers-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: re-evaluate derived props during teardown diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fdfc13c0e20a..e563c7753492 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -19,6 +19,7 @@ import { inspect_effects, set_inspect_effects } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { component_context } from '../context.js'; +import { UNINITIALIZED } from '../../../constants.js'; /** * @template V @@ -51,7 +52,7 @@ export function derived(fn) { fn, reactions: null, rv: 0, - v: /** @type {V} */ (null), + v: /** @type {V} */ (UNINITIALIZED), wv: 0, parent: parent_derived ?? active_effect, ac: null diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 3501bcd3c722..03daad5251f2 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,18 +1,26 @@ -/** @import { Derived, Source } from './types.js' */ +/** @import { ComponentContext } from '#client' */ +/** @import { Derived, Effect, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED + PROPS_IS_UPDATED, + UNINITIALIZED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { get, untrack } from '../runtime.js'; +import { + active_effect, + get, + is_destroying_effect, + set_active_effect, + untrack +} from '../runtime.js'; import * as e from '../errors.js'; -import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; +import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -92,7 +100,7 @@ export function rest_props(props, exclude, name) { /** * The proxy handler for legacy $$restProps and $$props - * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source }>}} + * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source, parent_effect: Effect }>}} */ const legacy_rest_props_handler = { get(target, key) { @@ -102,17 +110,25 @@ const legacy_rest_props_handler = { }, set(target, key, value) { if (!(key in target.special)) { - // Handle props that can temporarily get out of sync with the parent - /** @type {Record unknown>} */ - target.special[key] = prop( - { - get [key]() { - return target.props[key]; - } - }, - /** @type {string} */ (key), - PROPS_IS_UPDATED - ); + var previous_effect = active_effect; + + try { + set_active_effect(target.parent_effect); + + // Handle props that can temporarily get out of sync with the parent + /** @type {Record unknown>} */ + target.special[key] = prop( + { + get [key]() { + return target.props[key]; + } + }, + /** @type {string} */ (key), + PROPS_IS_UPDATED + ); + } finally { + set_active_effect(previous_effect); + } } target.special[key](value); @@ -151,7 +167,19 @@ const legacy_rest_props_handler = { * @returns {Record} */ export function legacy_rest_props(props, exclude) { - return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler); + return new Proxy( + { + props, + exclude, + special: {}, + version: source(0), + // TODO this is only necessary because we need to track component + // destruction inside `prop`, because of `bind:this`, but it + // seems likely that we can simplify `bind:this` instead + parent_effect: /** @type {Effect} */ (active_effect) + }, + legacy_rest_props_handler + ); } /** @@ -366,16 +394,24 @@ export function prop(props, key, flags, fallback) { // create a derived that we can write to locally. // Or we are in legacy mode where we always create a derived to replicate that // Svelte 4 did not trigger updates when a primitive value was updated to the same value. - var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); + var overridden = false; + + var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => { + overridden = false; + return getter(); + }); // Capture the initial value if it's bindable if (bindable) get(d); + var parent_effect = /** @type {Effect} */ (active_effect); + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; set(d, new_value); + overridden = true; if (fallback_value !== undefined) { fallback_value = new_value; @@ -384,8 +420,12 @@ export function prop(props, key, flags, fallback) { return value; } - // TODO is this still necessary post-#16263? - if (has_destroyed_component_ctx(d)) { + // special case — avoid recalculating the derived if we're in a + // teardown function and the prop was overridden locally, or the + // component was already destroyed (this latter part is necessary + // because `bind:this` can read props after the component has + // been destroyed. TODO simplify `bind:this` + if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) { return d.v; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c00ade558770..28fcf26e8354 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; -import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; +import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { tracing_mode_flag } from '../flags/index.js'; @@ -42,6 +42,7 @@ import { set_dev_stack } from './context.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; +import { UNINITIALIZED } from '../../constants.js'; let is_flushing = false; @@ -795,7 +796,7 @@ export function get(signal) { } } - if (is_derived) { + if (is_derived && !is_destroying_effect) { derived = /** @type {Derived} */ (signal); if (check_dirtiness(derived)) { @@ -836,13 +837,48 @@ export function get(signal) { } } - if (is_destroying_effect && old_values.has(signal)) { - return old_values.get(signal); + if (is_destroying_effect) { + if (old_values.has(signal)) { + return old_values.get(signal); + } + + if (is_derived) { + derived = /** @type {Derived} */ (signal); + + var value = derived.v; + + // if the derived is dirty, or depends on the values that just changed, re-execute + if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) { + value = execute_derived(derived); + } + + old_values.set(derived, value); + + return value; + } } return signal.v; } +/** @param {Derived} derived */ +function depends_on_old_values(derived) { + if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst + if (derived.deps === null) return false; + + for (const dep of derived.deps) { + if (old_values.has(dep)) { + return true; + } + + if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) { + return true; + } + } + + return false; +} + /** * Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared * @template V diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js new file mode 100644 index 000000000000..694dccdcf8a0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs, target }) { + const [increment] = target.querySelectorAll('button'); + + flushSync(() => increment.click()); + flushSync(() => increment.click()); + flushSync(() => increment.click()); + + assert.deepEqual(logs, [ + 'count: 1', + 'squared: 1', + 'count: 2', + 'squared: 4', + 'count: 3', + 'squared: 9', + 'count: 4' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte new file mode 100644 index 000000000000..a4c58a8e991c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte @@ -0,0 +1,20 @@ + + + + +

count: {count}

+ +{#if count % 2 === 0} +

squared: {squared}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte new file mode 100644 index 000000000000..3c863c7fc94c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte @@ -0,0 +1,15 @@ + + +

{count}

+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js new file mode 100644 index 000000000000..1f6e5d1f7f96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js @@ -0,0 +1,17 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [increment, toggle] = target.querySelectorAll('button'); + + flushSync(() => toggle.click()); + assert.deepEqual(logs, [0, 'hello']); + + flushSync(() => toggle.click()); + flushSync(() => increment.click()); + flushSync(() => increment.click()); + + assert.deepEqual(logs, [0, 'hello', 1, 'hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte new file mode 100644 index 000000000000..1a97d3e760d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte @@ -0,0 +1,13 @@ + + + + + +{#if count < 2 && message === 'hello'} + +{/if} From c9098bcaa0cf7accf7de4a16f8b72671023e69e7 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 9 Jul 2025 15:32:18 +0200 Subject: [PATCH 2/4] fix: use `state` instead of `source` in reactive classes (#16239) * fix: use `state` instead of `source` in reactive classes * fix: use `active_reaction` as indication to use `source` or `state` * fix: cleanup `#initial_reaction` on `teardown` to free memory * fix: use `#source` in `set` too * unused * chore: use WeakRef * use update_version instead of WeakRef in SvelteSet/SvelteMap (#16324) * tidy up * tweak comment to remove active_reaction reference --------- Co-authored-by: Rich Harris --- .../svelte/src/internal/client/runtime.js | 6 +- packages/svelte/src/motion/spring.js | 12 +- packages/svelte/src/motion/tweened.js | 6 +- packages/svelte/src/reactivity/map.js | 25 ++++- packages/svelte/src/reactivity/set.js | 18 ++- .../side-effect-derived-map/_config.js | 33 +++++- .../side-effect-derived-map/main.svelte | 106 +++++++++++++++--- .../side-effect-derived-set/_config.js | 12 +- .../side-effect-derived-set/main.svelte | 51 ++++++--- .../side-effect-derived-spring/_config.js | 23 ++++ .../side-effect-derived-spring/main.svelte | 26 +++++ .../side-effect-derived-tween/_config.js | 23 ++++ .../side-effect-derived-tween/main.svelte | 26 +++++ 13 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 28fcf26e8354..cd7170d32699 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -134,6 +134,8 @@ let write_version = 1; /** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */ let read_version = 0; +export let update_version = read_version; + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -267,6 +269,7 @@ export function update_reaction(reaction) { var previous_reaction_sources = source_ownership; var previous_component_context = component_context; var previous_untracking = untracking; + var previous_update_version = update_version; var flags = reaction.f; @@ -280,7 +283,7 @@ export function update_reaction(reaction) { source_ownership = null; set_component_context(reaction.ctx); untracking = false; - read_version++; + update_version = ++read_version; reaction.f |= EFFECT_IS_UPDATING; @@ -368,6 +371,7 @@ export function update_reaction(reaction) { source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + update_version = previous_update_version; reaction.f ^= EFFECT_IS_UPDATING; } diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 0f3bc6fb9f87..44be1a501b91 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } 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'; @@ -170,9 +170,9 @@ export function spring(value, opts = {}) { * @since 5.8.0 */ export class Spring { - #stiffness = source(0.15); - #damping = source(0.8); - #precision = source(0.01); + #stiffness = state(0.15); + #damping = state(0.8); + #precision = state(0.01); #current; #target; @@ -194,8 +194,8 @@ export class Spring { * @param {SpringOpts} [options] */ constructor(value, options = {}) { - this.#current = DEV ? tag(source(value), 'Spring.current') : source(value); - this.#target = DEV ? tag(source(value), 'Spring.target') : source(value); + this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); + this.#target = DEV ? tag(state(value), 'Spring.target') : state(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); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 09bd06c325d5..437c22ec3b2b 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js'; 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 { set, state } 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'; @@ -191,8 +191,8 @@ export class Tween { * @param {TweenedOptions} options */ constructor(value, options = {}) { - this.#current = source(value); - this.#target = source(value); + this.#current = state(value); + this.#target = state(value); this.#defaults = options; if (DEV) { diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index cd2fac163fc6..4fa2dfd7b2fc 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; /** @@ -56,6 +56,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -79,6 +80,19 @@ export class SvelteMap extends Map { } } + /** + * If the source is being created inside the same reaction as the SvelteMap instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + /** @param {K} key */ has(key) { var sources = this.#sources; @@ -87,7 +101,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -123,7 +137,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -154,7 +168,7 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -219,8 +233,7 @@ export class SvelteMap extends Map { if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { - var s = source(0); - + var s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 8a656c2bc14a..9e3c330beb36 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -50,6 +50,7 @@ export class SvelteSet extends Set { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -75,6 +76,19 @@ export class SvelteSet extends Set { if (!inited) this.#init(); } + /** + * If the source is being created inside the same reaction as the SvelteSet instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + // We init as part of the first instance so that we can treeshake this class #init() { inited = true; @@ -116,7 +130,7 @@ export class SvelteSet extends Set { return false; } - s = source(true); + s = this.#source(true); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js index 5ad6f57e311f..c10dc7fb555f 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js @@ -8,7 +8,8 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4, button5, button6, button7, button8] = + target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +20,35 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); + + assert.throws(() => { + button5?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button6?.click(); + flushSync(); + }); + + assert.throws(() => { + button7?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button8?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte index bdd5ccb75c91..c37f37ceb6b7 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte @@ -1,27 +1,101 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throw_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has} + {throw_has} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has} + {works_has} {/if} + +{#if outside_get} + {throw_get} +{/if} + +{#if inside_get} + {works_get} +{/if} + + +{#if outside_values} + {throw_values} +{/if} + +{#if inside_values} + {works_values} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js index 5ad6f57e311f..5cf066fb8a0c 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js @@ -8,7 +8,7 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4] = target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +19,15 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte index 8564f6e7c48e..1d6735ba64b4 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte @@ -1,27 +1,52 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has_delete} + {throws_has_delete} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has_delete} + {works_has_delete} {/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte new file mode 100644 index 000000000000..b0818deca960 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte new file mode 100644 index 000000000000..bd007f2b500d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file From 9d176b50080ed075d97ed96a15e2109b3db69886 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 09:37:29 -0400 Subject: [PATCH 3/4] chore: changeset for #16239 (#16325) --- .changeset/gorgeous-birds-brake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gorgeous-birds-brake.md diff --git a/.changeset/gorgeous-birds-brake.md b/.changeset/gorgeous-birds-brake.md new file mode 100644 index 000000000000..8365cc0e6a08 --- /dev/null +++ b/.changeset/gorgeous-birds-brake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction From fab2091743d48f682d470cccb1a99590ce6bac32 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:41:08 -0400 Subject: [PATCH 4/4] Version Packages (#16314) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/gorgeous-birds-brake.md | 5 ----- .changeset/light-rivers-jump.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/gorgeous-birds-brake.md delete mode 100644 .changeset/light-rivers-jump.md diff --git a/.changeset/gorgeous-birds-brake.md b/.changeset/gorgeous-birds-brake.md deleted file mode 100644 index 8365cc0e6a08..000000000000 --- a/.changeset/gorgeous-birds-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md deleted file mode 100644 index 2454d5715602..000000000000 --- a/.changeset/light-rivers-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: re-evaluate derived props during teardown diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 89ae38084029..19aa1466c069 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.5 + +### Patch Changes + +- fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction ([#16325](https://github.com/sveltejs/svelte/pull/16325)) + +- fix: re-evaluate derived props during teardown ([#16278](https://github.com/sveltejs/svelte/pull/16278)) + ## 5.35.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index aaf8f26fec2a..4ac497ed4131 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.35.4", + "version": "5.35.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c4111b9e8d41..eb68753d7129 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.35.4'; +export const VERSION = '5.35.5'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy