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 diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e3841f..cad966bd4f27 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,14 @@ 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. + if (hydrating) { + 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..066febe163c9 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,20 @@ 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, { - 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`); 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 +145,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) {