diff --git a/.changeset/itchy-eels-marry.md b/.changeset/itchy-eels-marry.md new file mode 100644 index 0000000000..59ea9d9dbe --- /dev/null +++ b/.changeset/itchy-eels-marry.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: remove memory leak from bind:this diff --git a/.changeset/lazy-knives-happen.md b/.changeset/lazy-knives-happen.md new file mode 100644 index 0000000000..61a7ebd9f8 --- /dev/null +++ b/.changeset/lazy-knives-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make snippet effects transparent for transitions diff --git a/.changeset/pre.json b/.changeset/pre.json index fa9642fd20..8a25b9de9c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -179,7 +179,9 @@ "large-clouds-carry", "large-turkeys-deny", "late-crabs-lay", + "late-grapes-judge", "late-peaches-mate", + "lazy-knives-happen", "lazy-masks-sit", "lazy-months-knock", "lazy-spiders-think", @@ -252,6 +254,7 @@ "polite-pumpkins-guess", "polite-ravens-study", "poor-eggs-enjoy", + "poor-hats-design", "poor-seahorses-flash", "popular-ligers-perform", "popular-mangos-rest", @@ -410,6 +413,7 @@ "unlucky-steaks-warn", "unlucky-trees-lick", "violet-pigs-jam", + "weak-drinks-speak", "weak-terms-destroy", "wet-games-fly", "wet-wombats-repeat", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a7ef780583..2e22b024d4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.0.0-next.106 + +### Patch Changes + +- feat: use state proxy ancestry for ownership validation ([#11184](https://github.com/sveltejs/svelte/pull/11184)) + +- fix: make snippet effects transparent for transitions ([#11195](https://github.com/sveltejs/svelte/pull/11195)) + +- fix: return ast from `compile` (like Svelte 4 does) ([#11191](https://github.com/sveltejs/svelte/pull/11191)) + +- fix: ensure bind:this unmount behavior for members is conditional ([#11193](https://github.com/sveltejs/svelte/pull/11193)) + ## 5.0.0-next.105 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index eb07f0f1a2..82f502b32b 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.0.0-next.105", + "version": "5.0.0-next.106", "type": "module", "types": "./types/index.d.ts", "engines": { 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 821c82d73a..1c73427463 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -565,7 +565,7 @@ function special(parser) { type: 'VariableDeclaration', kind: 'const', declarations: [{ type: 'VariableDeclarator', id, init }], - start: start + 1, + start: start + 2, // start at const, not at @const end: parser.index - 1 } }); diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 563e10384a..b9953b2ec6 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -10,7 +10,9 @@ export const DIRTY = 1 << 9; export const MAYBE_DIRTY = 1 << 10; export const INERT = 1 << 11; export const DESTROYED = 1 << 12; -export const IS_ELSEIF = 1 << 13; -export const EFFECT_RAN = 1 << 14; +export const EFFECT_RAN = 1 << 13; + +/** 'Transparent' effects do not create a transition boundary */ +export const EFFECT_TRANSPARENT = 1 << 14; export const STATE_SYMBOL = Symbol('$state'); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 277abacfbd..c432943c1a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,4 +1,4 @@ -import { IS_ELSEIF } from '../../constants.js'; +import { EFFECT_TRANSPARENT } from '../../constants.js'; import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; import { remove } from '../reconciler.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; @@ -79,6 +79,6 @@ export function if_block( }); if (elseif) { - effect.f |= IS_ELSEIF; + effect.f |= EFFECT_TRANSPARENT; } } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 9d494c4598..e885440910 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,3 +1,4 @@ +import { EFFECT_TRANSPARENT } from '../../constants.js'; import { branch, render_effect } from '../../reactivity/effects.js'; /** @@ -11,11 +12,13 @@ export function snippet(get_snippet, node, ...args) { /** @type {SnippetFn | null | undefined} */ var snippet; - render_effect(() => { + var effect = render_effect(() => { if (snippet === (snippet = get_snippet())) return; if (snippet) { branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args)); } }); + + effect.f |= EFFECT_TRANSPARENT; } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 940ac9f62f..b95d2a4239 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,6 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; +import { queue_task } from '../../task.js'; /** * @param {any} bound_value @@ -47,7 +48,8 @@ export function bind_this(element_or_component, update, get_value, get_parts) { }); return () => { - effect(() => { + // We cannot use effects in the teardown phase, we we use a microtask instead. + queue_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 0c0b4332e8..7ee9e33b56 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -7,7 +7,7 @@ import { should_intro } from '../../render.js'; import { is_function } from '../../utils.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; -import { EFFECT_RAN } from '../../constants.js'; +import { BLOCK_EFFECT, EFFECT_RAN } from '../../constants.js'; /** * @template T @@ -212,6 +212,11 @@ export function transition(flags, element, get_fn, get_params) { if (is_intro && should_intro) { var parent = /** @type {import('#client').Effect} */ (e.parent); + // e.g snippets are implemented as render effects — keep going until we find the parent block + while ((parent.f & BLOCK_EFFECT) === 0 && parent.parent) { + parent = parent.parent; + } + if (is_global || (parent.f & EFFECT_RAN) !== 0) { effect(() => { untrack(() => transition.in()); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 89307a4e56..c1a2a7e162 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,12 +1,9 @@ import { run_all } from '../../shared/utils.js'; let is_task_queued = false; -let is_raf_queued = false; /** @type {Array<() => void>} */ let current_queued_tasks = []; -/** @type {Array<() => void>} */ -let current_raf_tasks = []; function process_task() { is_task_queued = false; @@ -15,11 +12,15 @@ function process_task() { run_all(tasks); } -function process_raf_task() { - is_raf_queued = false; - const tasks = current_raf_tasks.slice(); - current_raf_tasks = []; - run_all(tasks); +/** + * @param {() => void} fn + */ +export function queue_task(fn) { + if (!is_task_queued) { + is_task_queued = true; + queueMicrotask(process_task); + } + current_queued_tasks.push(fn); } /** @@ -29,7 +30,4 @@ export function flush_tasks() { if (is_task_queued) { process_task(); } - if (is_raf_queued) { - process_raf_task(); - } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 6965049801..8e7bd12b04 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -7,9 +7,11 @@ import { destroy_effect_children, execute_effect, get, + is_destroying_effect, is_flushing_effect, remove_reactions, schedule_effect, + set_is_destroying_effect, set_is_flushing_effect, set_signal_status, untrack @@ -24,7 +26,7 @@ import { EFFECT_RAN, BLOCK_EFFECT, ROOT_EFFECT, - IS_ELSEIF + EFFECT_TRANSPARENT } from '../constants.js'; import { set } from './sources.js'; import { remove } from '../dom/reconciler.js'; @@ -109,6 +111,12 @@ export function user_effect(fn) { (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') ); } + if (is_destroying_effect) { + throw new Error( + 'ERR_SVELTE_EFFECT_IN_TEARDOWN' + + (DEV ? ': The Svelte $effect rune can not be used in the teardown phase of an effect.' : '') + ); + } // Non-nested `$effect(...)` in a component should be deferred // until the component is mounted @@ -140,6 +148,14 @@ export function user_pre_effect(fn) { : '') ); } + if (is_destroying_effect) { + throw new Error( + 'ERR_SVELTE_EFFECT_IN_TEARDOWN' + + (DEV + ? ': The Svelte $effect.pre rune can not be used in the teardown phase of an effect.' + : '') + ); + } return render_effect(fn); } @@ -228,6 +244,22 @@ export function branch(fn) { return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true); } +/** + * @param {import("#client").Effect} effect + */ +export function execute_effect_teardown(effect) { + var teardown = effect.teardown; + if (teardown !== null) { + const previously_destroying_effect = is_destroying_effect; + set_is_destroying_effect(true); + try { + teardown.call(null); + } finally { + set_is_destroying_effect(previously_destroying_effect); + } + } +} + /** * @param {import('#client').Effect} effect * @returns {void} @@ -249,7 +281,7 @@ export function destroy_effect(effect) { } } - effect.teardown?.call(null); + execute_effect_teardown(effect); var parent = effect.parent; @@ -345,7 +377,7 @@ export function pause_children(effect, transitions, local) { while (child !== null) { var sibling = child.next; - var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0; + var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0; // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. @@ -381,7 +413,7 @@ function resume_children(effect, local) { while (child !== null) { var sibling = child.next; - var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0; + var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0; // TODO we don't need to call resume_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e8bcc713d4..fe93eb47e4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -8,7 +8,12 @@ import { object_prototype } from './utils.js'; import { snapshot } from './proxy.js'; -import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js'; +import { + destroy_effect, + effect, + execute_effect_teardown, + user_pre_effect +} from './reactivity/effects.js'; import { EFFECT, RENDER_EFFECT, @@ -37,12 +42,18 @@ let current_scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; export let is_flushing_effect = false; +export let is_destroying_effect = false; /** @param {boolean} value */ export function set_is_flushing_effect(value) { is_flushing_effect = value; } +/** @param {boolean} value */ +export function set_is_destroying_effect(value) { + is_destroying_effect = value; +} + // Used for $inspect export let is_batching_effect = false; let is_inspecting_signal = false; @@ -406,7 +417,7 @@ export function execute_effect(effect) { destroy_effect_children(effect); } - effect.teardown?.call(null); + execute_effect_teardown(effect); var teardown = execute_reaction_fn(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; } finally { @@ -658,11 +669,11 @@ export function flush_sync(fn, flush_previous = true) { var result = fn?.(); + flush_tasks(); if (current_queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } - flush_tasks(); flush_count = 0; return result; diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index aea5aa86ba..e2cea9e696 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.105'; +export const VERSION = '5.0.0-next.106'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/transition-snippet/Container.svelte b/packages/svelte/tests/runtime-runes/samples/transition-snippet/Container.svelte new file mode 100644 index 0000000000..4948c25c83 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-snippet/Container.svelte @@ -0,0 +1,12 @@ + + + + +{#if visible} + {@render children()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/transition-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-snippet/_config.js new file mode 100644 index 0000000000..76a062043f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-snippet/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + test({ assert, target, raf }) { + const button = target.querySelector('button'); + ok(button); + + flushSync(() => button.click()); + raf.tick(50); + assert.htmlEqual(target.innerHTML, '
hello
'); + + flushSync(() => button.click()); + raf.tick(75); + assert.htmlEqual( + target.innerHTML, + 'hello
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-snippet/main.svelte new file mode 100644 index 0000000000..9a5bec1eec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-snippet/main.svelte @@ -0,0 +1,9 @@ + + +hello
+