From e1578d477849e44d8cd94bf97de4c097f0212957 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Wed, 15 Oct 2025 20:04:05 -0700 Subject: [PATCH 1/5] fix: hydrate boundaries inside async components --- .../src/internal/client/dom/blocks/boundary.js | 10 ++++++++-- .../boundary-in-async-component/_config.js | 11 +++++++++++ .../boundary-in-async-component/main.svelte | 12 ++++++++++++ packages/svelte/tests/hydration/test.ts | 17 +++++++++++++---- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js create mode 100644 packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e384..b904da9b6d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -22,7 +22,8 @@ import { hydrating, next, skip_nodes, - set_hydrate_node + set_hydrate_node, + set_hydrating } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; @@ -191,7 +192,12 @@ export class Boundary { Batch.enqueue(() => { this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => { + // We've already hydrated the pending content. We stop hydrating + // here so the resolved content is rendered on top of it. + set_hydrating(false); + return this.#children(this.#anchor) + }); }); if (this.#pending_count > 0) { diff --git a/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js b/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js new file mode 100644 index 0000000000..bdb8dc6cf7 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async: true, + async test(assert, target) { + assert.htmlEqual(target.innerHTML, 'component: loaded, boundary: loading'); + await tick(); + assert.htmlEqual(target.innerHTML, 'component: loaded, boundary: loaded'); + } +}); diff --git a/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte b/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte new file mode 100644 index 0000000000..7ab0b2b027 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte @@ -0,0 +1,12 @@ + + +component: loaded, boundary: + + + loaded + {#snippet pending()} + loading + {/snippet} + diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d072..db87906591 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -10,8 +10,10 @@ import { createClassComponent } from 'svelte/legacy'; import { render } from 'svelte/server'; import type { CompileOptions } from '#compiler'; import { flushSync } from 'svelte'; +import type { RenderOutput, SyncRenderOutput } from '#server'; interface HydrationTest extends BaseTest { + async?: boolean; load_compiled?: boolean; server_props?: Record; id_prefix?: string; @@ -43,6 +45,11 @@ function read(path: string): string | void { } const { test, run } = suite(async (config, cwd) => { + if (config.async) { + config.compileOptions ??= {}; + config.compileOptions.experimental ??= {}; + config.compileOptions.experimental.async = true; + } if (!config.load_compiled) { await compile_directory(cwd, 'client', { accessors: true, @@ -56,16 +63,18 @@ 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, { + + let rendered: RenderOutput | SyncRenderOutput = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { props: config.server_props ?? config.props ?? {}, idPrefix: config?.id_prefix }); + if (config.async) rendered = await rendered; 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'); @@ -134,7 +143,7 @@ const { test, run } = suite(async (config, cwd) => { const normalize = (string: string) => string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + const expected = read(`${cwd}/_expected.html`) ?? rendered.body; assert.equal(normalize(target.innerHTML), normalize(expected)); if (rendered.head) { From 62b9a831955ac766b3c022a81e43e76754947060 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Wed, 15 Oct 2025 20:08:45 -0700 Subject: [PATCH 2/5] fix treeshaking --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b904da9b6d..a6a95cb58c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -195,7 +195,9 @@ export class Boundary { return branch(() => { // We've already hydrated the pending content. We stop hydrating // here so the resolved content is rendered on top of it. - set_hydrating(false); + if (hydrating) { + set_hydrating(false); + } return this.#children(this.#anchor) }); }); From 26c288ec342e4563bb9374365da31d437e27b7a1 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Wed, 15 Oct 2025 20:09:05 -0700 Subject: [PATCH 3/5] format --- .../src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/tests/hydration/test.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a6a95cb58c..cad966bd4f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -198,7 +198,7 @@ export class Boundary { if (hydrating) { set_hydrating(false); } - return this.#children(this.#anchor) + return this.#children(this.#anchor); }); }); diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index db87906591..066febe163 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -63,11 +63,13 @@ const { test, run } = suite(async (config, cwd) => { const target = window.document.body; const head = window.document.head; - - let rendered: RenderOutput | SyncRenderOutput = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { - props: config.server_props ?? config.props ?? {}, - idPrefix: config?.id_prefix - }); + let rendered: RenderOutput | SyncRenderOutput = render( + (await import(`${cwd}/_output/server/main.svelte.js`)).default, + { + props: config.server_props ?? config.props ?? {}, + idPrefix: config?.id_prefix + } + ); if (config.async) rendered = await rendered; const override = read(`${cwd}/_override.html`); From 99f0b1f44c1d64db1c25714e218d8a7050a67b6e Mon Sep 17 00:00:00 2001 From: Ottomated Date: Wed, 15 Oct 2025 20:09:21 -0700 Subject: [PATCH 4/5] changeset --- .changeset/silver-llamas-hear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silver-llamas-hear.md diff --git a/.changeset/silver-llamas-hear.md b/.changeset/silver-llamas-hear.md new file mode 100644 index 0000000000..f937eb053c --- /dev/null +++ b/.changeset/silver-llamas-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: hydrate boundaries inside async components correctly From 06049a96afb53257a913da6a75c7ed40b32d5289 Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:38:30 -0700 Subject: [PATCH 5/5] Merge --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 55a8b274d8..d29627891c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -215,7 +215,7 @@ export class Boundary { if (hydrating) { set_hydrating(false); } - return this.#children(this.#anchor); + return this.#children(anchor); }); });