fix: properly hydrate already-resolved async blocks (alternative) (#17641)

This is basically #17611, minus #17640, plus #17639. We need to add the $.next() call after render tags as well as components; rather than duplicating the logic, we can use is_standalone to determine when this is necessary (since this is what prevents $.append(...) from being used).

Fixes #17261
Fixes #17608
---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/17644/head
Rich Harris 2 days ago committed by GitHub
parent f5304ec8c9
commit bc44975869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: properly hydrate already-resolved async blocks

@ -166,6 +166,7 @@ export function client_component(analysis, options) {
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
is_standalone: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),

@ -83,6 +83,9 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
/** True if the current node is a) a component or render tag and b) the sole child of a block */
readonly is_standalone: boolean;
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;

@ -122,7 +122,10 @@ export function Fragment(node, context) {
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
process_children(trimmed, () => b.id('$$anchor'), false, {
...context,
state: { ...state, is_standalone }
});
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);

@ -85,6 +85,10 @@ export function RenderTag(node, context) {
)
)
);
if (context.state.is_standalone) {
context.state.init.push(b.stmt(b.call('$.next')));
}
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}

@ -461,7 +461,7 @@ export function build_component(node, component_name, loc, context) {
memoizer.check_blockers(node.metadata.expression);
}
const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
let statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (is_component_dynamic) {
const prev = fn;
@ -515,15 +515,21 @@ export function build_component(node, component_name, loc, context) {
const blockers = memoizer.blockers();
if (async_values || blockers) {
return b.stmt(
b.call(
'$.async',
anchor,
blockers,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
statements = [
b.stmt(
b.call(
'$.async',
anchor,
blockers,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)
)
);
];
if (context.state.is_standalone) {
statements.push(b.stmt(b.call('$.next')));
}
}
return statements.length > 1 ? b.block(statements) : statements[0];

@ -101,10 +101,16 @@ export function build_inline_component(node, expression, context) {
}
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
} else if (attribute.type === 'BindDirective') {
// Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
optimiser.check_blockers(attribute.metadata.expression);
if (attribute.name === 'this') {
// bind:this is client-only, but we still need to check for blockers to ensure
// the server generates matching hydration markers if the client wraps in $.async
continue;
}
if (attribute.expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
.expressions;

@ -0,0 +1,4 @@
<script>
let { children } = $props()
</script>
<div>{@render children?.()}</div>

@ -0,0 +1,4 @@
<script>
let { children } = $props()
</script>
<div>{@render children?.()}</div>

@ -0,0 +1,7 @@
<script lang='ts'>
import type { Attachment } from 'svelte/attachments'
export function action(): Attachment<HTMLElement> {
return ()=>{}
}
</script>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<div><div>foo</div></div>');
}
});

@ -0,0 +1,16 @@
<script>
import Outer from './Outer.svelte'
import Inner from './Inner.svelte'
import Trigger from './Trigger.svelte'
const data = $derived(await Promise.resolve(['a', 'b']))
let trigger = $state()
</script>
<Outer>
<Inner {@attach trigger?.action}>
foo
</Inner>
</Outer>
<Trigger bind:this={trigger} />

@ -0,0 +1,5 @@
<script>
let { message, another } = $props()
</script>
<p>{message}</p>

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
ssrHtml: `<p>item 1</p><p>item 2</p><p>item 3</p>`,
html: `<p>item 1</p><p>item 2</p><p>item 3</p>`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>item 1</p><p>item 2</p><p>item 3</p>');
}
});

@ -0,0 +1,10 @@
<script>
import Component from './Component.svelte'
const messages = await Promise.resolve(["item 1", "item 2", "item 3"])
const another = { test: 'test' }
</script>
{#each messages as message}
<Component {message} {another} />
{/each}
Loading…
Cancel
Save