From bdb1a0f847ba4f04252e9d26a7690dbfb0683da7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:59:57 +0200 Subject: [PATCH] fix: ensure async block assigns correct nodes to effect (#18371) If a block has a sole component child, we apply an optimization to not need a template. But if that sole child is wrapped in `$.async`, it can happens that the block's effect is assigned the wrong end node. In the reproduction this happens because we have two components inside a Child which is rendered only after an async block resolves. In `$.append` we have logic to not reassign nodes when the active effect didn't already run, which it has by the time we come across it (because the async work had to resolve first; we added this in https://github.com/sveltejs/svelte/pull/17120 to prevent the opposite, nodes being "rewinded" to an earlier position) and so the second sibling component's `$.append` will not set its end node to the effect. As a result the end node is wrong (too early). To fix this we assign the nodes in `$.async`. We could also use `compareDocumentPosition` in `$.append` to only ever go forward, but that feels a bit heavier performance-wise. --------- Co-authored-by: paoloricciuti Co-authored-by: Rich Harris --- .changeset/async-hydration-orphan-nodes.md | 5 +++ .../src/internal/client/dom/blocks/async.js | 2 + packages/svelte/tests/hydration/test.ts | 8 ++-- .../samples/async-sole-if-child/Child.svelte | 7 ++++ .../samples/async-sole-if-child/Footer.svelte | 1 + .../samples/async-sole-if-child/Header.svelte | 1 + .../samples/async-sole-if-child/_config.js | 38 +++++++++++++++++++ .../samples/async-sole-if-child/main.svelte | 11 ++++++ 8 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 .changeset/async-hydration-orphan-nodes.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Footer.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Header.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-sole-if-child/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-sole-if-child/main.svelte diff --git a/.changeset/async-hydration-orphan-nodes.md b/.changeset/async-hydration-orphan-nodes.md new file mode 100644 index 0000000000..4c35d6b74e --- /dev/null +++ b/.changeset/async-hydration-orphan-nodes.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly track effect end node for async sibling component \ No newline at end of file diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 170529a6b9..78e2ed44be 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -9,6 +9,7 @@ import { set_hydrating, skip_nodes } from '../hydration.js'; +import { assign_nodes } from '../template.js'; /** * @param {TemplateNode} node @@ -23,6 +24,7 @@ export function async(node, blockers = [], expressions = [], fn) { if (was_hydrating) { hydrate_next(); end = skip_nodes(false); + assign_nodes(node, end); // Necessary if this wraps the sole child of a block, else end marker can be wrong } if (expressions.length === 0 && blockers.every((b) => b.settled)) { diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index d2bf6fde34..0050130817 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -72,7 +72,7 @@ const { test, run } = suite(async (config, cwd) => { const target = window.document.body; const head = window.document.head; - const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { + const rendered = await render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { props: config.server_props ?? config.props ?? {}, idPrefix: config?.id_prefix }); @@ -80,8 +80,8 @@ const { test, run } = suite(async (config, cwd) => { const override = read(`${cwd}/_override.html`); const override_head = read(`${cwd}/_override_head.html`); - fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n'); - target.innerHTML = override ?? rendered.html; + fs.writeFileSync(`${cwd}/_output/body.html`, rendered.body + '\n'); + target.innerHTML = override ?? rendered.body; if (rendered.head) { fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n'); @@ -145,7 +145,7 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + const expected = read(`${cwd}/_expected.html`) ?? rendered.body; assert_html_equal(target.innerHTML, expected); if (rendered.head) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte new file mode 100644 index 0000000000..304b7bd8a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-sole-if-child/Child.svelte @@ -0,0 +1,7 @@ + + +
+