From ad19a1a0f5e4fccab6e2acd05e88eb58169ff74e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:50:59 -0700 Subject: [PATCH 01/16] fix: don't clone non-proxies in `$inspect` (#16617) * fix: don't clone non-proxies in `$inspect` * apply suggestion * lint --- .changeset/itchy-games-guess.md | 5 ++++ packages/svelte/src/internal/shared/clone.js | 27 +++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 .changeset/itchy-games-guess.md diff --git a/.changeset/itchy-games-guess.md b/.changeset/itchy-games-guess.md new file mode 100644 index 0000000000..44516cfc21 --- /dev/null +++ b/.changeset/itchy-games-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't clone non-proxies in `$inspect` diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 4632fc3d68..b8f99ee198 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -15,14 +15,15 @@ const empty = []; * @template T * @param {T} value * @param {boolean} [skip_warning] + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -export function snapshot(value, skip_warning = false) { +export function snapshot(value, skip_warning = false, no_tojson = false) { if (DEV && !skip_warning) { /** @type {string[]} */ const paths = []; - const copy = clone(value, new Map(), '', paths); + const copy = clone(value, new Map(), '', paths, null, no_tojson); if (paths.length === 1 && paths[0] === '') { // value could not be cloned w.state_snapshot_uncloneable(); @@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) { return copy; } - return clone(value, new Map(), '', empty); + return clone(value, new Map(), '', empty, null, no_tojson); } /** @@ -49,10 +50,11 @@ export function snapshot(value, skip_warning = false) { * @param {Map>} cloned * @param {string} path * @param {string[]} paths - * @param {null | T} original The original value, if `value` was produced from a `toJSON` call + * @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -function clone(value, cloned, path, paths, original = null) { +function clone(value, cloned, path, paths, original = null, no_tojson = false) { if (typeof value === 'object' && value !== null) { var unwrapped = cloned.get(value); if (unwrapped !== undefined) return unwrapped; @@ -71,7 +73,7 @@ function clone(value, cloned, path, paths, original = null) { for (var i = 0; i < value.length; i += 1) { var element = value[i]; if (i in value) { - copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths); + copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson); } } @@ -88,8 +90,15 @@ function clone(value, cloned, path, paths, original = null) { } for (var key in value) { - // @ts-expect-error - copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths); + copy[key] = clone( + // @ts-expect-error + value[key], + cloned, + DEV ? `${path}.${key}` : path, + paths, + null, + no_tojson + ); } return copy; @@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) { return /** @type {Snapshot} */ (structuredClone(value)); } - if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') { + if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) { return clone( /** @type {T & { toJSON(): any } } */ (value).toJSON(), cloned, From a543559acfb21ecc509873790158ba16db4089fd Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:36:29 -0700 Subject: [PATCH 02/16] fix: avoid recursion error when tagging circular references (#16622) * fix: avoid recursion error when tagging circular references * try suggestion * add some logging * make logging clearer * more * try this * add test * tweak * fix? * fix?? --- .changeset/six-shirts-scream.md | 5 ++++ packages/svelte/src/internal/client/proxy.js | 11 +++++--- .../_config.js | 26 +++++++++++++++++++ .../main.svelte | 10 +++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .changeset/six-shirts-scream.md create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/main.svelte diff --git a/.changeset/six-shirts-scream.md b/.changeset/six-shirts-scream.md new file mode 100644 index 0000000000..ac4d183474 --- /dev/null +++ b/.changeset/six-shirts-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid recursion error when tagging circular references diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 3ae4b87ed5..dae3791eb0 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -93,9 +93,11 @@ export function proxy(value) { /** Used in dev for $inspect.trace() */ var path = ''; - + let updating = false; /** @param {string} new_path */ function update_path(new_path) { + if (updating) return; + updating = true; path = new_path; tag(version, `${path} version`); @@ -104,6 +106,7 @@ export function proxy(value) { for (const [prop, source] of sources) { tag(source, get_label(path, prop)); } + updating = false; } return new Proxy(/** @type {any} */ (value), { @@ -284,13 +287,13 @@ export function proxy(value) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); - set(s, proxy(value)); - - sources.set(prop, s); if (DEV) { tag(s, get_label(path, prop)); } + set(s, proxy(value)); + + sources.set(prop, s); } } else { has = s.v !== UNINITIALIZED; diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/_config.js new file mode 100644 index 0000000000..ca81c7854a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/_config.js @@ -0,0 +1,26 @@ +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, logs }) { + const files = { id: 1, items: [{ id: 2, items: [{ id: 3 }, { id: 4 }] }] }; + // @ts-expect-error + files.items[0].parent = files; + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'test (main.svelte:5:4)' }, + { log: '$state', highlighted: true }, + { log: 'filesState.files', highlighted: false }, + { log: files }, + { log: '$state', highlighted: true }, + { log: 'filesState.files.items[0].parent.items', highlighted: false }, + { log: files.items }, + { log: '$state', highlighted: true }, + { log: 'filesState.files.items[0].parent.items[0]', highlighted: false }, + { log: files.items[0] } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/main.svelte new file mode 100644 index 0000000000..7640d48f77 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-circular-reference/main.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file From 036d4588abb0b053a22a156664b92474244dfe47 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:33:55 -0700 Subject: [PATCH 03/16] fix: actually use the changes from #16617 (#16626) * quick fix * fix failing test --- packages/svelte/src/internal/client/dev/inspect.js | 2 +- .../samples/inspect-recursive-2/main.svelte | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index c593f2622c..f79cf47299 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) { return; } - var snap = snapshot(value, true); + var snap = snapshot(value, true, true); untrack(() => { inspector(initial ? 'init' : 'update', ...snap); }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte index f7874d2192..23a1c237c3 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte @@ -1,21 +1,15 @@ + +

A

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte new file mode 100644 index 0000000000..459aa313c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte @@ -0,0 +1,8 @@ + + +

B

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js new file mode 100644 index 0000000000..e750c48a00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target, logs }) { + const [button] = target.querySelectorAll('button'); + + flushSync(() => button.click()); + assert.deepEqual(logs, ['create A', 'destroy A', 'create B']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte new file mode 100644 index 0000000000..c9ef5430a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte @@ -0,0 +1,13 @@ + + + + + From 7b2d7746277dae6372ed7b836ad895bde536dc89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 15 Aug 2025 17:09:33 -0400 Subject: [PATCH 05/16] chore: update/simplify test (#16630) * update/simplify test * oops * simplify effect-order-7 --- .../samples/effect-order-6/A.svelte | 7 ++++--- .../samples/effect-order-6/B.svelte | 4 +--- .../samples/effect-order-6/Child.svelte | 20 ------------------- .../samples/effect-order-6/_config.js | 2 +- .../samples/effect-order-6/main.svelte | 20 ++++++++++++------- .../samples/effect-order-7/A.svelte | 6 +++--- .../samples/effect-order-7/B.svelte | 4 +--- .../samples/effect-order-7/Child.svelte | 20 ------------------- .../samples/effect-order-7/main.svelte | 19 ++++++++++++------ 9 files changed, 36 insertions(+), 66 deletions(-) delete mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte index 2e789a0460..22b8ed0f20 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte @@ -1,11 +1,12 @@ - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte index 1fad19bc15..2233961177 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.boolean} - - {@render children(object.boolean)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js index 8f9077e954..91e1569e46 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js @@ -8,6 +8,6 @@ export default test({ flushSync(() => open.click()); flushSync(() => close.click()); - assert.deepEqual(logs, [true]); + assert.deepEqual(logs, [{ boolean: true, closed: false }]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte index eee487fa13..08f6fc48da 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte @@ -1,6 +1,15 @@ @@ -15,9 +24,6 @@
- - {#snippet children(boolean)} - - {/snippet} - - +{#if object} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte index 54f4869d62..b55c8f4115 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte @@ -1,9 +1,9 @@ -{boolean} +{boolean} {closed} - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte index 2a2e634db1..7b33c342f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.nested} - - {@render children(object.nested)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte index c9c45c50cf..d9e9eb17ad 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte @@ -1,6 +1,15 @@ @@ -15,8 +24,6 @@
- - {#snippet children(nested)} -
- {/snippet} - +{#if object} + +{/if} From 2b85d2a544573d66c09a77c4100087cc183ae0fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 17 Aug 2025 08:31:20 -0400 Subject: [PATCH 06/16] fix: run blocks eagerly during flush (#16631) fixes #16548 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/clever-months-clap.md | 5 +++ .../src/internal/client/dom/operations.js | 2 ++ .../src/internal/client/reactivity/batch.js | 36 ++++++++++--------- .../src/internal/client/reactivity/sources.js | 8 ++++- .../samples/effect-loop-3/Child.svelte | 13 +++++++ .../samples/effect-loop-3/_config.js | 12 +++++++ .../samples/effect-loop-3/main.svelte | 15 ++++++++ 7 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 .changeset/clever-months-clap.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte diff --git a/.changeset/clever-months-clap.md b/.changeset/clever-months-clap.md new file mode 100644 index 0000000000..4a1b69e824 --- /dev/null +++ b/.changeset/clever-months-clap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: run blocks eagerly during flush instead of aborting diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index fb269e47e0..abc29a7670 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { async_mode_flag } from '../../flags/index.js'; import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; +import { eager_block_effects } from '../reactivity/batch.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -214,6 +215,7 @@ export function clear_text_content(node) { */ export function should_defer_append() { if (!async_mode_flag) return false; + if (eager_block_effects !== null) return false; var flags = /** @type {Effect} */ (active_effect).f; return (flags & EFFECT_RAN) !== 0; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 123bc95d16..60fa03c56c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -292,12 +292,12 @@ export class Batch { if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; + } else if ((flags & EFFECT) !== 0) { + this.#effects.push(effect); + } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + this.#render_effects.push(effect); } else if ((flags & CLEAN) === 0) { - if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); - } else if ((flags & ASYNC) !== 0) { + if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); } else if (is_dirty(effect)) { @@ -584,6 +584,9 @@ function infinite_loop_guard() { } } +/** @type {Effect[] | null} */ +export let eager_block_effects = null; + /** * @param {Array} effects * @returns {void} @@ -598,7 +601,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - var n = current_batch ? current_batch.current.size : 0; + eager_block_effects = []; update_effect(effect); @@ -619,21 +622,20 @@ function flush_queued_effects(effects) { } } - // if state is written in a user effect, abort and re-schedule, lest we run - // effects that should be removed as a result of the state change - if ( - current_batch !== null && - current_batch.current.size > n && - (effect.f & USER_EFFECT) !== 0 - ) { - break; + if (eager_block_effects.length > 0) { + // TODO this feels incorrect! it gets the tests passing + old_values.clear(); + + for (const e of eager_block_effects) { + update_effect(e); + } + + eager_block_effects = []; } } } - while (i < length) { - schedule_effect(effects[i++]); - } + eager_block_effects = null; } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 7fb3135708..cd0c28016d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,7 +33,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch, schedule_effect } from './batch.js'; +import { Batch, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -334,6 +334,12 @@ function mark_reactions(signal, status) { if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else if (not_dirty) { + if ((flags & BLOCK_EFFECT) !== 0) { + if (eager_block_effects !== null) { + eager_block_effects.push(/** @type {Effect} */ (reaction)); + } + } + schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte new file mode 100644 index 0000000000..9bf4db52d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte @@ -0,0 +1,13 @@ + + +{#if inited} + {@render children()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js new file mode 100644 index 0000000000..046c190432 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + assert.doesNotThrow(() => { + flushSync(() => button.click()); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte new file mode 100644 index 0000000000..2b3a171798 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if show} + {#each { length: 1234 } as i} + {i} + {/each} +{/if} From f5d4350f3d60606c556e195a80f21427a4b6738c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 Aug 2025 08:52:44 -0400 Subject: [PATCH 07/16] Revert "fix: do not defer unmount; immediately unmount components (#16624)" (#16639) This reverts commit 95e51755814329d596ced98fb1829d0f797e503d. --- .changeset/few-geese-itch.md | 5 ----- .../internal/client/dom/blocks/svelte-component.js | 10 +++++----- .../dynamic-component-destroy-then-create/A.svelte | 8 -------- .../dynamic-component-destroy-then-create/B.svelte | 8 -------- .../_config.js | 13 ------------- .../main.svelte | 13 ------------- 6 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 .changeset/few-geese-itch.md delete mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js delete mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte diff --git a/.changeset/few-geese-itch.md b/.changeset/few-geese-itch.md deleted file mode 100644 index 737aa01911..0000000000 --- a/.changeset/few-geese-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: destroy dynamic component instance before creating new one diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index be6611c96d..2697722b39 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -34,6 +34,11 @@ export function component(node, get_component, render_fn) { var pending_effect = null; function commit() { + if (effect) { + pause_effect(effect); + effect = null; + } + if (offscreen_fragment) { // remove the anchor /** @type {Text} */ (offscreen_fragment.lastChild).remove(); @@ -51,11 +56,6 @@ export function component(node, get_component, render_fn) { var defer = should_defer_append(); - if (effect) { - pause_effect(effect); - effect = null; - } - if (component) { var target = anchor; diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte deleted file mode 100644 index e73551cc28..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/A.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -

A

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte deleted file mode 100644 index 459aa313c4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/B.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -

B

diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js deleted file mode 100644 index e750c48a00..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/_config.js +++ /dev/null @@ -1,13 +0,0 @@ -import { test } from '../../test'; -import { flushSync } from 'svelte'; - -export default test({ - mode: ['client', 'hydrate'], - - async test({ assert, target, logs }) { - const [button] = target.querySelectorAll('button'); - - flushSync(() => button.click()); - assert.deepEqual(logs, ['create A', 'destroy A', 'create B']); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte deleted file mode 100644 index c9ef5430a4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-component-destroy-then-create/main.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - From 04836a87995f0674ae4f105b7761fd19f7ee3b6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:56:46 -0400 Subject: [PATCH 08/16] Version Packages (#16603) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-months-clap.md | 5 ----- .changeset/itchy-games-guess.md | 5 ----- .changeset/six-shirts-scream.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/clever-months-clap.md delete mode 100644 .changeset/itchy-games-guess.md delete mode 100644 .changeset/six-shirts-scream.md diff --git a/.changeset/clever-months-clap.md b/.changeset/clever-months-clap.md deleted file mode 100644 index 4a1b69e824..0000000000 --- a/.changeset/clever-months-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: run blocks eagerly during flush instead of aborting diff --git a/.changeset/itchy-games-guess.md b/.changeset/itchy-games-guess.md deleted file mode 100644 index 44516cfc21..0000000000 --- a/.changeset/itchy-games-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't clone non-proxies in `$inspect` diff --git a/.changeset/six-shirts-scream.md b/.changeset/six-shirts-scream.md deleted file mode 100644 index ac4d183474..0000000000 --- a/.changeset/six-shirts-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid recursion error when tagging circular references diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index bb61b50dd5..cd6a4a916c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.38.2 + +### Patch Changes + +- perf: run blocks eagerly during flush instead of aborting ([#16631](https://github.com/sveltejs/svelte/pull/16631)) + +- fix: don't clone non-proxies in `$inspect` ([#16617](https://github.com/sveltejs/svelte/pull/16617)) + +- fix: avoid recursion error when tagging circular references ([#16622](https://github.com/sveltejs/svelte/pull/16622)) + ## 5.38.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8904c103e3..fc7db9598d 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.38.1", + "version": "5.38.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cf71ae709a..2aa62504a5 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.38.1'; +export const VERSION = '5.38.2'; export const PUBLIC_VERSION = '5'; From 1dcced59e79dd04163ce4aa380948ca6eea777af Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:10:08 +0200 Subject: [PATCH 09/16] fix: properly catch top level await errors (#16619) * fix: properly catch top level await errors async errors within the template and derived etc are properly handled because they know about the last active effect and invoke the error boundary correctly as a response. This logic was missing for our top level await output. Fixes #16613 * test * use helper for async bodies (#16641) * use helper for async bodies * unused * fix * failing test + fix --------- Co-authored-by: Simon Holthausen --------- Co-authored-by: Rich Harris --- .changeset/silent-suns-whisper.md | 5 ++ .../3-transform/client/transform-client.js | 60 +++++++------------ packages/svelte/src/internal/client/index.js | 5 +- .../src/internal/client/reactivity/async.js | 21 ++++++- .../src/internal/client/reactivity/effects.js | 3 +- .../Child.svelte | 9 +++ .../_config.js | 15 +++++ .../main.svelte | 18 ++++++ .../async-top-level-error-nested/Child.svelte | 7 +++ .../async-top-level-error-nested/_config.js | 14 +++++ .../async-top-level-error-nested/main.svelte | 18 ++++++ 11 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 .changeset/silent-suns-whisper.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte diff --git a/.changeset/silent-suns-whisper.md b/.changeset/silent-suns-whisper.md new file mode 100644 index 0000000000..7ee7d74abc --- /dev/null +++ b/.changeset/silent-suns-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly catch top level await errors diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 166207f66a..940d6a9e00 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -359,16 +359,31 @@ export function client_component(analysis, options) { if (dev) push_args.push(b.id(analysis.name)); let component_block = b.block([ + store_init, ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - analysis.runes || !analysis.needs_context - ? b.empty - : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) + ...state.instance_level_snippets ]); + if (analysis.instance.has_await) { + const body = b.block([ + .../** @type {ESTree.Statement[]} */ (instance.body), + b.if(b.call('$.aborted'), b.return()), + .../** @type {ESTree.Statement[]} */ (template.body) + ]); + + component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + } else { + component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body)); + + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); + } + + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + } + if (analysis.needs_mutation_validation) { component_block.body.unshift( b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) @@ -389,41 +404,6 @@ export function client_component(analysis, options) { analysis.uses_slots || analysis.slot_names.size > 0; - if (analysis.instance.has_await) { - const params = [b.id('$$anchor')]; - if (should_inject_props) { - params.push(b.id('$$props')); - } - if (store_setup.length > 0) { - params.push(b.id('$$stores')); - } - const body = b.function_declaration( - b.id('$$body'), - params, - b.block([ - b.var('$$unsuspend', b.call('$.suspend')), - ...component_block.body, - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body), - b.stmt(b.call('$$unsuspend')) - ]), - true - ); - - state.hoisted.push(body); - - component_block = b.block([ - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - store_init, - b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } else { - component_block.body.unshift(store_init); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); - } - // trick esrap into including comments component_block.loc = instance.loc; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c094c9e044..c5b7bb845c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -99,6 +99,7 @@ export { with_script } from './dom/template.js'; export { + async_body, for_await_track_reactivity_loss, save, track_reactivity_loss @@ -151,7 +152,8 @@ export { untrack, exclude_from_object, deep_read, - deep_read_state + deep_read_state, + active_effect } from './runtime.js'; export { validate_binding, validate_each_keys } from './validate.js'; export { raf } from './timing.js'; @@ -176,3 +178,4 @@ export { } from '../shared/validate.js'; export { strict_equals, equals } from './dev/equality.js'; export { log_if_contains_state } from './dev/console-log.js'; +export { invoke_error_boundary } from './error-handling.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2b133e5f44..1ea1bbe561 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch } from './batch.js'; +import { current_batch, suspend } from './batch.js'; import { async_derived, current_async_effect, @@ -19,6 +19,7 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; +import { aborted } from './effects.js'; /** * @@ -170,3 +171,21 @@ export function unset_context() { set_component_context(null); if (DEV) set_from_async_derived(null); } + +/** + * @param {() => Promise} fn + */ +export async function async_body(fn) { + const unsuspend = suspend(); + const active = /** @type {Effect} */ (active_effect); + + try { + await fn(); + } catch (error) { + if (!aborted(active)) { + invoke_error_boundary(error, active); + } + } finally { + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 68a1555032..df3dd75808 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -648,7 +648,6 @@ function resume_children(effect, local) { } } -export function aborted() { - var effect = /** @type {Effect} */ (active_effect); +export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte new file mode 100644 index 0000000000..f7ba132ace --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js new file mode 100644 index 0000000000..298e33e9a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reject] = target.querySelectorAll('button'); + + await tick(); + reject.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

route: other

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte new file mode 100644 index 0000000000..2f461e96c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte @@ -0,0 +1,18 @@ + + + + + + {#if route.current === 'home'} + + {:else} +

route: {route.current}

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte new file mode 100644 index 0000000000..11c9ebd653 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js new file mode 100644 index 0000000000..57005b4112 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reject] = target.querySelectorAll('button'); + + reject.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

failed

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte new file mode 100644 index 0000000000..2fdf4c0d2f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte @@ -0,0 +1,18 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed()} +

failed

+ {/snippet} +
From a60995abefc52a88493580e8b7f390efeb84456c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:28:43 +0200 Subject: [PATCH 10/16] fix: allow async `{@const}` in more places (#16643) Implemented by reusing the `async_body` function inside `Fragment.js`. Also removes the ability to reference a `{@const ...}` of an implicit child inside a boundary pending/failed snippet: - existing duplication of consts can have unintended side effects, e.g. async consts would unexpectedly called multiple times - what if a const is the reason for the failure of a boundary, but is then referenced in the failed snippet? - what if an async const is referenced in a pending snippet? deadlock - inconsistent with how it behaves for components where this already does not work Implemented via the experimental flag so the behavior change only applies there as this is a breaking change strictly speaking. Also added a compiler error for this. closes #16462 --- .changeset/light-camels-push.md | 5 ++ .../98-reference/.generated/compile-errors.md | 45 +++++++++++++ .../messages/compile-errors/template.md | 43 ++++++++++++ packages/svelte/src/compiler/errors.js | 10 +++ .../phases/2-analyze/visitors/Identifier.js | 33 ++++++++++ .../3-transform/client/visitors/Fragment.js | 11 +--- .../client/visitors/SvelteBoundary.js | 66 ++++++++++++------- packages/svelte/src/compiler/phases/nodes.js | 9 +++ packages/svelte/src/compiler/phases/scope.js | 3 +- .../_config.js | 10 +++ .../main.svelte | 32 +++++++++ .../_config.js | 10 +++ .../main.svelte | 27 ++++++++ packages/svelte/tests/compiler-errors/test.ts | 4 +- .../samples/async-const/_config.js | 2 +- .../samples/async-const/main.svelte | 10 +++ .../FlakyComponent.svelte | 2 +- .../_config.js | 18 +++++ .../main.svelte | 14 ++++ .../samples/const-tag-boundary/_config.js | 2 +- .../samples/const-tag-boundary/main.svelte | 14 ++-- .../input.svelte | 6 +- 22 files changed, 328 insertions(+), 48 deletions(-) create mode 100644 .changeset/light-camels-push.md create mode 100644 packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte rename packages/svelte/tests/runtime-runes/samples/{const-tag-boundary => const-tag-boundary-deprecated-usage}/FlakyComponent.svelte (74%) create mode 100644 packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte diff --git a/.changeset/light-camels-push.md b/.changeset/light-camels-push.md new file mode 100644 index 0000000000..cac7f5a51e --- /dev/null +++ b/.changeset/light-camels-push.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow async `{@const}` in more places diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 957a9f67c7..b9c44163c9 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle% `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` ``` +### const_tag_invalid_reference + +``` +The `{@const %name% = ...}` declaration is not available in this snippet +``` + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ### constant_assignment ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 0569f63ad3..dc26a02767 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -124,6 +124,49 @@ > `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` +## const_tag_invalid_reference + +> The `{@const %name% = ...}` declaration is not available in this snippet + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ## debug_tag_invalid_arguments > {@debug ...} arguments must be identifiers, not arbitrary expressions diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index e763a6e073..44fc641ee5 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -985,6 +985,16 @@ export function const_tag_invalid_placement(node) { e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`\`, \`\`\nhttps://svelte.dev/e/const_tag_invalid_placement`); } +/** + * The `{@const %name% = ...}` declaration is not available in this snippet + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function const_tag_invalid_reference(node, name) { + e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`); +} + /** * {@debug ...} arguments must be identifiers, not arbitrary expressions * @param {null | number | NodeLike} node 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 4dfdfe5af1..1c98a95e63 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -7,6 +7,7 @@ import * as w from '../../../warnings.js'; import { is_rune } from '../../../../utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { get_rune } from '../../scope.js'; +import { is_component_node } from '../../nodes.js'; /** * @param {Identifier} node @@ -155,5 +156,37 @@ export function Identifier(node, context) { ) { w.reactive_declaration_module_script_dependency(node); } + + if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) { + let snippet_name; + + // Find out if this references a {@const ...} declaration of an implicit children snippet + // when it is itself inside a snippet block at the same level. If so, error. + for (let i = context.path.length - 1; i >= 0; i--) { + const parent = context.path[i]; + const grand_parent = context.path[i - 1]; + + if (parent.type === 'SnippetBlock') { + snippet_name = parent.expression.name; + } else if ( + snippet_name && + grand_parent && + parent.type === 'Fragment' && + (is_component_node(grand_parent) || + (grand_parent.type === 'SvelteBoundary' && + (snippet_name === 'failed' || snippet_name === 'pending'))) + ) { + if ( + is_component_node(grand_parent) + ? grand_parent.metadata.scopes.default === binding.scope + : context.state.scopes.get(parent) === binding.scope + ) { + e.const_tag_invalid_reference(node, node.name); + } else { + break; + } + } + } + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index c7c576101e..85d8e3caff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -51,7 +51,6 @@ export function Fragment(node, context) { const has_await = context.state.init !== null && (node.metadata.has_await || false); const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent - const unsuspend = b.id('$$unsuspend'); /** @type {Statement[]} */ const body = []; @@ -151,10 +150,6 @@ export function Fragment(node, context) { } } - if (has_await) { - body.push(b.var(unsuspend, b.call('$.suspend'))); - } - body.push(...state.consts); if (has_await) { @@ -182,8 +177,8 @@ export function Fragment(node, context) { } if (has_await) { - body.push(b.stmt(b.call(unsuspend))); + return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]); + } else { + return b.block(body); } - - return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 70df022355..49c89bc438 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -39,40 +39,60 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const hoisted = []; + let has_const = false; + // const tags need to live inside the boundary, but might also be referenced in hoisted snippets. // to resolve this we cheat: we duplicate const tags inside snippets + // We'll revert this behavior in the future, it was a mistake to allow this (Component snippets also don't do this). for (const child of node.fragment.nodes) { if (child.type === 'ConstTag') { - context.visit(child, { ...context.state, consts: const_tags }); + has_const = true; + if (!context.state.options.experimental.async) { + context.visit(child, { ...context.state, consts: const_tags }); + } } } for (const child of node.fragment.nodes) { if (child.type === 'ConstTag') { + if (context.state.options.experimental.async) { + nodes.push(child); + } continue; } if (child.type === 'SnippetBlock') { - /** @type {Statement[]} */ - const statements = []; - - context.visit(child, { ...context.state, init: statements }); - - const snippet = /** @type {VariableDeclaration} */ (statements[0]); - - const snippet_fn = dev - ? // @ts-expect-error we know this shape is correct - snippet.declarations[0].init.arguments[1] - : snippet.declarations[0].init; - - snippet_fn.body.body.unshift( - ...const_tags.filter((node) => node.type === 'VariableDeclaration') - ); - - hoisted.push(snippet); - - if (['failed', 'pending'].includes(child.expression.name)) { - props.properties.push(b.prop('init', child.expression, child.expression)); + if ( + context.state.options.experimental.async && + has_const && + !['failed', 'pending'].includes(child.expression.name) + ) { + // we can't hoist snippets as they may reference const tags, so we just keep them in the fragment + nodes.push(child); + } else { + /** @type {Statement[]} */ + const statements = []; + + context.visit(child, { ...context.state, init: statements }); + + const snippet = /** @type {VariableDeclaration} */ (statements[0]); + + const snippet_fn = dev + ? // @ts-expect-error we know this shape is correct + snippet.declarations[0].init.arguments[1] + : snippet.declarations[0].init; + + if (!context.state.options.experimental.async) { + snippet_fn.body.body.unshift( + ...const_tags.filter((node) => node.type === 'VariableDeclaration') + ); + } + + if (['failed', 'pending'].includes(child.expression.name)) { + props.properties.push(b.prop('init', child.expression, child.expression)); + } + + hoisted.push(snippet); } continue; @@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) { const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); - block.body.unshift(...const_tags); + if (!context.state.options.experimental.async) { + block.body.unshift(...const_tags); + } const boundary = b.stmt( b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 4874554ff0..f4127db359 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -23,6 +23,15 @@ export function is_element_node(node) { return element_nodes.includes(node.type); } +/** + * Returns true for all component-like nodes + * @param {AST.SvelteNode} node + * @returns {node is AST.Component | AST.SvelteComponent | AST.SvelteSelf} + */ +export function is_component_node(node) { + return ['Component', 'SvelteComponent', 'SvelteSelf'].includes(node.type); +} + /** * @param {AST.RegularElement | AST.SvelteElement} node * @returns {boolean} diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f88f5ef8b1..76157d406f 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -122,7 +122,7 @@ export class Binding { /** * Additional metadata, varies per binding type - * @type {null | { inside_rest?: boolean }} + * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }} */ metadata = null; @@ -1121,6 +1121,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { node.kind, declarator.init ); + binding.metadata = { is_template_declaration: true }; bindings.push(binding); } } diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js new file mode 100644 index 0000000000..7424278180 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async: true, + error: { + code: 'const_tag_invalid_reference', + message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + position: [376, 379] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte new file mode 100644 index 0000000000..a2533e33b0 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte @@ -0,0 +1,32 @@ + + + + + {@const foo = 'bar'} + + {#snippet other()} + {foo} + {/snippet} + + {foo} + + + {#snippet failed()} + {foo} + {/snippet} + + + {#snippet failed()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js new file mode 100644 index 0000000000..7ff71a61f9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async: true, + error: { + code: 'const_tag_invalid_reference', + message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + position: [298, 301] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte new file mode 100644 index 0000000000..c59df28ec9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte @@ -0,0 +1,27 @@ + + + + + {@const foo = 'bar'} + {foo} + + + {#snippet prop()} + {foo} + {/snippet} + + + {#snippet prop()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet prop()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/test.ts b/packages/svelte/tests/compiler-errors/test.ts index 13b9280dde..b3a2d4af31 100644 --- a/packages/svelte/tests/compiler-errors/test.ts +++ b/packages/svelte/tests/compiler-errors/test.ts @@ -5,6 +5,7 @@ import { suite, type BaseTest } from '../suite'; import { read_file } from '../helpers.js'; interface CompilerErrorTest extends BaseTest { + async?: boolean; error: { code: string; message: string; @@ -29,7 +30,8 @@ const { test, run } = suite((config, cwd) => { try { compile(read_file(`${cwd}/main.svelte`), { - generate: 'client' + generate: 'client', + experimental: { async: config.async ?? false } }); } catch (e) { const error = e as CompileError; diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 084d9c3874..8aeca875f3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -7,6 +7,6 @@ export default test({ async test({ assert, target }) { await tick(); - assert.htmlEqual(target.innerHTML, `

Hello, world!

`); + assert.htmlEqual(target.innerHTML, `

Hello, world!

5 01234`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 9321bd7929..7410ff6a6f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -3,6 +3,8 @@ + {@const number = await Promise.resolve(5)} + {#snippet pending()}

Loading...

{/snippet} @@ -10,6 +12,14 @@ {#snippet greet()} {@const greeting = await `Hello, ${name}!`}

{greeting}

+ {number} + {#if number > 4} + {@const length = await number} + {#each { length }, index} + {@const i = await index} + {i} + {/each} + {/if} {/snippet} {@render greet()} diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte similarity index 74% rename from packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte rename to packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte index 8bbec90de4..ea60542af9 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte @@ -1,3 +1,3 @@ \ No newline at end of file + diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js new file mode 100644 index 0000000000..915bda91f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_async: true, + html: '

2

', + mode: ['client'], + test({ target, assert }) { + const btn = target.querySelector('button'); + const p = target.querySelector('p'); + + flushSync(() => { + btn?.click(); + }); + + assert.equal(p?.innerHTML, '4'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte new file mode 100644 index 0000000000..25ea8a3ffc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte @@ -0,0 +1,14 @@ + + + + + + {@const double = test * 2} + {#snippet failed()} +

{double}

+ {/snippet} + +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js index 4338969a48..e4ffb4a850 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js @@ -2,7 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - html: '

2

', + html: '

2

', mode: ['client'], test({ target, assert }) { const btn = target.querySelector('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte index 25ea8a3ffc..9605e12070 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte @@ -1,14 +1,10 @@ - + - {@const double = test * 2} - {#snippet failed()} -

{double}

- {/snippet} - -
\ No newline at end of file + {@const double = count * 2} +

{double}

+
diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte index 5708cc36ca..c965a379e5 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte +++ b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte @@ -4,8 +4,6 @@ {@const x = a} - {#snippet failed()} - {x} - {/snippet} + {x} - \ No newline at end of file +
From 11a2d8e9371b2a1b456f05af7cd4adc0bf212e62 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:33:48 -0700 Subject: [PATCH 11/16] fix: only emit `for_await_track_reactivity_loss` in async mode (#16644) Helps with #16610 (but does not fix it yet) --- .changeset/tender-masks-bow.md | 5 +++++ .../phases/3-transform/client/visitors/ForOfStatement.js | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-masks-bow.md diff --git a/.changeset/tender-masks-bow.md b/.changeset/tender-masks-bow.md new file mode 100644 index 0000000000..224db5fc37 --- /dev/null +++ b/.changeset/tender-masks-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: only emit `for_await_track_reactivity_loss` in async mode diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js index a5d2751812..8ae67f49d1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js @@ -8,7 +8,12 @@ import { dev, is_ignored } from '../../../../state.js'; * @param {ComponentContext} context */ export function ForOfStatement(node, context) { - if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) { + if ( + node.await && + dev && + !is_ignored(node, 'await_reactivity_loss') && + context.state.options.experimental.async + ) { const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left)); const argument = /** @type {Expression} */ (context.visit(node.right)); const body = /** @type {Statement} */ (context.visit(node.body)); From acd9eaf2ec8d964fa5770fdbcd76e41809197848 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:37:57 +0200 Subject: [PATCH 12/16] fix: ensure correct order of template effect values (#16655) Compiler does sync then async but `memoizer.apply` did it the other way around --- .changeset/healthy-crabs-marry.md | 5 +++++ .../3-transform/client/visitors/shared/utils.js | 2 +- .../async-template-async-sync-mixed/_config.js | 9 +++++++++ .../async-template-async-sync-mixed/main.svelte | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .changeset/healthy-crabs-marry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte diff --git a/.changeset/healthy-crabs-marry.md b/.changeset/healthy-crabs-marry.md new file mode 100644 index 0000000000..eab29dae4a --- /dev/null +++ b/.changeset/healthy-crabs-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure correct order of template effect values 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 014547cf2d..ba140a153e 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 @@ -34,7 +34,7 @@ export class Memoizer { } apply() { - return [...this.#async, ...this.#sync].map((memo, i) => { + return [...this.#sync, ...this.#async].map((memo, i) => { memo.id.name = `$${i}`; return memo.id; }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js new file mode 100644 index 0000000000..709b88578f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

foo bar

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte new file mode 100644 index 0000000000..2e0ae46f1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte @@ -0,0 +1,17 @@ + + + +

{foo()} {await bar()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
From d3cb1482fe45a2b80b189856d8ab8a1f2c446bc6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Aug 2025 16:29:10 -0400 Subject: [PATCH 13/16] perf: better effect pruning (#16625) * tweak * prune effects where possible * tweak * simplify * simplify * changeset * reset parent if necessary --- .changeset/tasty-lizards-care.md | 5 ++ .../src/internal/client/reactivity/effects.js | 57 +++++++++++-------- 2 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 .changeset/tasty-lizards-care.md diff --git a/.changeset/tasty-lizards-care.md b/.changeset/tasty-lizards-care.md new file mode 100644 index 0000000000..b6aff07ffb --- /dev/null +++ b/.changeset/tasty-lizards-care.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: prune effects without dependencies diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index df3dd75808..2c9e4db911 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -133,29 +133,40 @@ function create_effect(type, fn, sync, push = true) { schedule_effect(effect); } - // if an effect has no dependencies, no DOM and no teardown function, - // don't bother adding it to the effect tree - var inert = - sync && - effect.deps === null && - effect.first === null && - effect.nodes_start === null && - effect.teardown === null && - (effect.f & EFFECT_PRESERVED) === 0; - - if (!inert && push) { - if (parent !== null) { - push_effect(effect, parent); - } + if (push) { + /** @type {Effect | null} */ + var e = effect; - // if we're in a derived, add the effect there too + // if an effect has already ran and doesn't need to be kept in the tree + // (because it won't re-run, has no DOM, and has no teardown etc) + // then we skip it and go to its child (if any) if ( - active_reaction !== null && - (active_reaction.f & DERIVED) !== 0 && - (type & ROOT_EFFECT) === 0 + sync && + e.deps === null && + e.teardown === null && + e.nodes_start === null && + e.first === e.last && // either `null`, or a singular child + (e.f & EFFECT_PRESERVED) === 0 ) { - var derived = /** @type {Derived} */ (active_reaction); - (derived.effects ??= []).push(effect); + e = e.first; + } + + if (e !== null) { + e.parent = parent; + + if (parent !== null) { + push_effect(e, parent); + } + + // if we're in a derived, add the effect there too + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { + var derived = /** @type {Derived} */ (active_reaction); + (derived.effects ??= []).push(e); + } } } @@ -242,7 +253,7 @@ export function inspect_effect(fn) { */ export function effect_root(fn) { Batch.ensure(); - const effect = create_effect(ROOT_EFFECT, fn, true); + const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true); return () => { destroy_effect(effect); @@ -256,7 +267,7 @@ export function effect_root(fn) { */ export function component_root(fn) { Batch.ensure(); - const effect = create_effect(ROOT_EFFECT, fn, true); + const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true); return (options = {}) => { return new Promise((fulfil) => { @@ -375,7 +386,7 @@ export function block(fn, flags = 0) { * @param {boolean} [push] */ export function branch(fn, push = true) { - return create_effect(BRANCH_EFFECT, fn, true, push); + return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true, push); } /** From e883cd086bd5f93b086220c7f2e2304bcb958eb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:24:51 -0700 Subject: [PATCH 14/16] Version Packages (#16642) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/healthy-crabs-marry.md | 5 ----- .changeset/light-camels-push.md | 5 ----- .changeset/silent-suns-whisper.md | 5 ----- .changeset/tasty-lizards-care.md | 5 ----- .changeset/tender-masks-bow.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/healthy-crabs-marry.md delete mode 100644 .changeset/light-camels-push.md delete mode 100644 .changeset/silent-suns-whisper.md delete mode 100644 .changeset/tasty-lizards-care.md delete mode 100644 .changeset/tender-masks-bow.md diff --git a/.changeset/healthy-crabs-marry.md b/.changeset/healthy-crabs-marry.md deleted file mode 100644 index eab29dae4a..0000000000 --- a/.changeset/healthy-crabs-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure correct order of template effect values diff --git a/.changeset/light-camels-push.md b/.changeset/light-camels-push.md deleted file mode 100644 index cac7f5a51e..0000000000 --- a/.changeset/light-camels-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow async `{@const}` in more places diff --git a/.changeset/silent-suns-whisper.md b/.changeset/silent-suns-whisper.md deleted file mode 100644 index 7ee7d74abc..0000000000 --- a/.changeset/silent-suns-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: properly catch top level await errors diff --git a/.changeset/tasty-lizards-care.md b/.changeset/tasty-lizards-care.md deleted file mode 100644 index b6aff07ffb..0000000000 --- a/.changeset/tasty-lizards-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: prune effects without dependencies diff --git a/.changeset/tender-masks-bow.md b/.changeset/tender-masks-bow.md deleted file mode 100644 index 224db5fc37..0000000000 --- a/.changeset/tender-masks-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: only emit `for_await_track_reactivity_loss` in async mode diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index cd6a4a916c..fb6b20c489 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.38.3 + +### Patch Changes + +- fix: ensure correct order of template effect values ([#16655](https://github.com/sveltejs/svelte/pull/16655)) + +- fix: allow async `{@const}` in more places ([#16643](https://github.com/sveltejs/svelte/pull/16643)) + +- fix: properly catch top level await errors ([#16619](https://github.com/sveltejs/svelte/pull/16619)) + +- perf: prune effects without dependencies ([#16625](https://github.com/sveltejs/svelte/pull/16625)) + +- fix: only emit `for_await_track_reactivity_loss` in async mode ([#16644](https://github.com/sveltejs/svelte/pull/16644)) + ## 5.38.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index fc7db9598d..b7effe35bd 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.38.2", + "version": "5.38.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2aa62504a5..815b25bf1f 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.38.2'; +export const VERSION = '5.38.3'; export const PUBLIC_VERSION = '5'; From 57fed6a267b719ff6ac49f9204a48e754f07fb3b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 25 Aug 2025 03:21:38 -0700 Subject: [PATCH 15/16] fix: place instance-level snippets inside async body (#16666) * fix: place instance-level snippets inside async body * lint --- .changeset/cool-garlics-fail.md | 5 +++++ .../phases/3-transform/client/transform-client.js | 9 ++++++--- .../samples/async-reference-in-snippet/_config.js | 9 +++++++++ .../samples/async-reference-in-snippet/app.svelte | 9 +++++++++ .../samples/async-reference-in-snippet/main.svelte | 8 ++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changeset/cool-garlics-fail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte diff --git a/.changeset/cool-garlics-fail.md b/.changeset/cool-garlics-fail.md new file mode 100644 index 0000000000..cabff1840d --- /dev/null +++ b/.changeset/cool-garlics-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: place instance-level snippets inside async body diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 940d6a9e00..bdd7eb3f17 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -362,12 +362,12 @@ export function client_component(analysis, options) { store_init, ...store_setup, ...legacy_reactive_declarations, - ...group_binding_declarations, - ...state.instance_level_snippets + ...group_binding_declarations ]); if (analysis.instance.has_await) { const body = b.block([ + ...state.instance_level_snippets, .../** @type {ESTree.Statement[]} */ (instance.body), b.if(b.call('$.aborted'), b.return()), .../** @type {ESTree.Statement[]} */ (template.body) @@ -375,7 +375,10 @@ export function client_component(analysis, options) { component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); } else { - component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body)); + component_block.body.push( + ...state.instance_level_snippets, + .../** @type {ESTree.Statement[]} */ (instance.body) + ); if (!analysis.runes && analysis.needs_context) { component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js new file mode 100644 index 0000000000..c6903c3eed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'value'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte new file mode 100644 index 0000000000..27b29cfe50 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte @@ -0,0 +1,9 @@ + + +{#snippet valueSnippet()} + {value} +{/snippet} + +{@render valueSnippet()} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte new file mode 100644 index 0000000000..c251a5645b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte @@ -0,0 +1,8 @@ + + + {#snippet pending()} + {/snippet} + + \ No newline at end of file From 71057368289e146e825327751a0a47a6f854eed7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Aug 2025 14:29:39 -0400 Subject: [PATCH 16/16] fix: restore batch along with effect context (#16668) Fixes https://github.com/sveltejs/svelte/issues/16596 Fixes https://github.com/sveltejs/kit/issues/14124 --- .changeset/silent-pigs-relax.md | 5 +++ .../src/internal/client/reactivity/async.js | 6 ++- .../src/internal/client/reactivity/batch.js | 4 +- .../samples/async-nested-top-level/Bar.svelte | 7 ++++ .../samples/async-nested-top-level/Foo.svelte | 10 +++++ .../samples/async-nested-top-level/_config.js | 42 +++++++++++++++++++ .../async-nested-top-level/main.svelte | 31 ++++++++++++++ .../samples/async-redirect/_config.js | 2 + .../samples/async-redirect/main.svelte | 4 ++ .../async-top-level-deriveds/Foo.svelte | 8 ++++ .../async-top-level-deriveds/_config.js | 41 ++++++++++++++++++ .../async-top-level-deriveds/main.svelte | 31 ++++++++++++++ 12 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 .changeset/silent-pigs-relax.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte diff --git a/.changeset/silent-pigs-relax.md b/.changeset/silent-pigs-relax.md new file mode 100644 index 0000000000..5acf185ffe --- /dev/null +++ b/.changeset/silent-pigs-relax.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: restore batch along with effect context diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 1ea1bbe561..65d004137f 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -73,11 +73,13 @@ function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; + var previous_batch = current_batch; return function restore() { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); + previous_batch?.activate(); if (DEV) { set_from_async_derived(null); @@ -176,8 +178,8 @@ export function unset_context() { * @param {() => Promise} fn */ export async function async_body(fn) { - const unsuspend = suspend(); - const active = /** @type {Effect} */ (active_effect); + var unsuspend = suspend(); + var active = /** @type {Effect} */ (active_effect); try { await fn(); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 60fa03c56c..2c60fc8313 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -76,8 +76,8 @@ let queued_root_effects = []; let last_scheduled_effect = null; let is_flushing = false; - let is_flushing_sync = false; + export class Batch { /** * The current values of any sources that are updated in this batch @@ -678,6 +678,8 @@ export function suspend() { if (!pending) { batch.activate(); batch.decrement(); + } else { + batch.deactivate(); } unset_context(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte new file mode 100644 index 0000000000..f1ac9ab760 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte @@ -0,0 +1,7 @@ + + +

bar: {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte new file mode 100644 index 0000000000..e2029a3033 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte @@ -0,0 +1,10 @@ + + +

foo: {foo}

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js new file mode 100644 index 0000000000..ca7965bf79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve] = target.querySelectorAll('button'); + + show.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo: foo

+

bar: bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js index ebbe642860..fe92977c21 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -29,6 +29,7 @@ export default test({

c

+

b or c

` ); @@ -46,6 +47,7 @@ export default test({

b

+

b or c

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte index bf5fdf9ed3..aead1b00e5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte @@ -33,6 +33,10 @@

c

{/if} + {#if route === 'b' || route === 'c'} +

b or c

+ {/if} + {#snippet pending()}

pending...

{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte new file mode 100644 index 0000000000..e8a7c84137 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte @@ -0,0 +1,8 @@ + + +

{foo} {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js new file mode 100644 index 0000000000..2c7ffd3952 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve] = target.querySelectorAll('button'); + + show.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

+ {/snippet} +