From e1578d477849e44d8cd94bf97de4c097f0212957 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Wed, 15 Oct 2025 20:04:05 -0700 Subject: [PATCH 1/4] 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 ca6437e3841f..b904da9b6dce 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 000000000000..bdb8dc6cf716 --- /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 000000000000..7ab0b2b02746 --- /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 70d5c5d0724f..db879065916f 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/4] 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 b904da9b6dce..a6a95cb58c09 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/4] 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 a6a95cb58c09..cad966bd4f27 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 db879065916f..066febe163c9 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/4] 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 000000000000..f937eb053c02 --- /dev/null +++ b/.changeset/silver-llamas-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: hydrate boundaries inside async components correctly