fix: propagate async @const blockers through closure references (#18309)

Right now, if you have the following:

```svelte
{@const data = await foo}
<p>{(() => data)()}<p>
```

It will blow up during CSR, because it tries to read `data` before it
exists. The solution to this is to consider references inside closures
as blockers for those closures. This _does_ mean we'll overblock in some
circumstances, such as:

```svelte
{@const data = await foo}
<button onclick={() => data}>foo<button>
```

But I wonder if that's actually incorrect? If the user were to click
`onclick` before `data` is ready, it would blow up.

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18311/head
Elliott Johnson 2 months ago committed by GitHub
parent ec08dbc7ee
commit e705369dee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: propagate async `@const` blockers through closure references so template expressions like `{(() => host)()}` correctly wait for the awaited value

@ -52,7 +52,7 @@ export class Memoizer {
* @param {ExpressionMetadata} metadata
*/
check_blockers(metadata) {
for (const binding of metadata.dependencies) {
for (const binding of metadata.references) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}

@ -102,8 +102,8 @@ export class ExpressionMetadata {
if (!this.#blockers) {
this.#blockers = new Set();
for (const d of this.dependencies) {
if (d.blocker) this.#blockers.add(d.blocker);
for (const r of this.references) {
if (r.blocker) this.#blockers.add(r.blocker);
}
}

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Tests that an IIFE referencing an `await` @const inside an {#each} block
// correctly registers the async dependency so the template waits for the
// resolved value instead of rendering with `undefined`.
export default test({
mode: ['client', 'hydrate', 'async-server'],
ssrHtml: '<p>example.com</p>',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>example.com</p>');
}
});

@ -0,0 +1,10 @@
<script>
async function get_host() {
return 'example.com';
}
</script>
{#each { length: 1 } as nothing}
{@const host = await get_host()}
<p>{(() => host)()}</p>
{/each}
Loading…
Cancel
Save