From 8bfd3a58410ab707496e2fa66e53642f3cb34716 Mon Sep 17 00:00:00 2001 From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:44:38 -0400 Subject: [PATCH] fix: block declaration tags on async values read inside closures --- .changeset/thick-plums-wave.md | 5 +++++ .../phases/2-analyze/visitors/DeclarationTag.js | 3 ++- .../client/visitors/DeclarationTag.js | 2 +- .../server/visitors/DeclarationTag.js | 2 +- .../async-declaration-closure-read/_config.js | 10 ++++++++++ .../async-declaration-closure-read/main.svelte | 17 +++++++++++++++++ .../_expected.html | 1 + .../async-declaration-closure-read/main.svelte | 17 +++++++++++++++++ 8 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 .changeset/thick-plums-wave.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/main.svelte diff --git a/.changeset/thick-plums-wave.md b/.changeset/thick-plums-wave.md new file mode 100644 index 0000000000..97a47ab883 --- /dev/null +++ b/.changeset/thick-plums-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: block declaration tags on async values read inside closures diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js index 52e9480838..2f72b16c72 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js @@ -43,7 +43,8 @@ export function DeclarationTag(node, context) { */ export function mark_async_declaration(context, metadata, declarations) { const has_await = metadata.expression.has_await; - const blockers = [...metadata.expression.dependencies] + // reads inside closures must block too, like they do in template expressions + const blockers = [...metadata.expression.references] .map((dep) => dep.blocker) .filter((b) => b !== null && b.object !== context.state.async_consts?.id); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js index abec5e828f..24a90544a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js @@ -70,7 +70,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); } - const blockers = [...metadata.expression.dependencies] + const blockers = [...metadata.expression.references] .map((dep) => dep.blocker) .filter((b) => b !== null && b.object !== context.state.async_consts?.id); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js index 8338700c0b..3fbff7167b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js @@ -66,7 +66,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name)); } - const blockers = [...metadata.expression.dependencies] + const blockers = [...metadata.expression.references] .map((dep) => dep.blocker) .filter((b) => b !== null && b.object !== context.state.async_consts?.id); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/_config.js b/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/_config.js new file mode 100644 index 0000000000..0c0beb3b7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// #18469 — a sync $derived in a nested snippet reading an async declaration through a closure must block on it +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '
true
false
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/main.svelte new file mode 100644 index 0000000000..ddd9a10b04 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/main.svelte @@ -0,0 +1,17 @@ + + +{#snippet outer()} + {const value = $derived(await getValue())} + {#snippet inner(keys)} + {const all_present = $derived(keys.every((k) => value.has(k)))} +{all_present}
+ {/snippet} + {@render inner(['a', 'b'])} + {@render inner(['a', 'x'])} +{/snippet} + +{@render outer()} diff --git a/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/_expected.html new file mode 100644 index 0000000000..d69b513938 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/_expected.html @@ -0,0 +1 @@ +true
false
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/main.svelte new file mode 100644 index 0000000000..ddd9a10b04 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-declaration-closure-read/main.svelte @@ -0,0 +1,17 @@ + + +{#snippet outer()} + {const value = $derived(await getValue())} + {#snippet inner(keys)} + {const all_present = $derived(keys.every((k) => value.has(k)))} +{all_present}
+ {/snippet} + {@render inner(['a', 'b'])} + {@render inner(['a', 'x'])} +{/snippet} + +{@render outer()}