diff --git a/.changeset/thick-plums-wave.md b/.changeset/thick-plums-wave.md new file mode 100644 index 0000000000..d8b454bb90 --- /dev/null +++ b/.changeset/thick-plums-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: block declaration tags and `{@const}` 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 3f78b53c90..ceb81a468d 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 @@ -71,7 +71,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-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 5c2d0dfe1c..de6749c0e7 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -177,11 +177,11 @@ export function runtime_suite(runes: boolean) { ['dom', 'hydrate', 'ssr', 'async-ssr'], (variant, config, test_name) => { if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { - return true; + return 'no-test'; } if (async_mode && config.skip_async) { - return true; + return 'no-test'; } if (variant === 'hydrate') { @@ -195,9 +195,9 @@ export function runtime_suite(runes: boolean) { ) { return 'no-test'; } - if (variant === 'ssr') { if ( + (test_name.startsWith('async-') && !config.mode?.includes('server')) || (config.mode && !config.mode.includes('server')) || (!config.test_ssr && config.html === undefined && diff --git a/packages/svelte/tests/runtime-runes/samples/async-const-closure-read/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const-closure-read/_config.js new file mode 100644 index 0000000000..be0e3c7f40 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const-closure-read/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// #18469 — a @const 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-const-closure-read/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const-closure-read/main.svelte new file mode 100644 index 0000000000..881455ec27 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const-closure-read/main.svelte @@ -0,0 +1,12 @@ + + +{#each [['a', 'b'], ['a', 'x']] as keys} + {@const all_present = keys.every((k) => value.has(k))} +

{all_present}

+{/each} 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..6a10f6928a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-closure-read/_config.js @@ -0,0 +1,11 @@ +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({ + ssrHtml: '

true

false

', + 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()}