diff --git a/.changeset/silly-mammals-fold.md b/.changeset/silly-mammals-fold.md new file mode 100644 index 0000000000..3513b4339e --- /dev/null +++ b/.changeset/silly-mammals-fold.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: repair dynamic component truthy/falsy hydration mismatches diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 6a2c6eb0be..b0e086b7ad 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -1,7 +1,14 @@ /** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ -import { empty_comment, build_attribute_value, PromiseOptimiser } from './utils.js'; +import { + empty_comment, + build_attribute_value, + PromiseOptimiser, + block_open_else, + block_open, + block_close +} from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; @@ -300,9 +307,22 @@ export function build_inline_component(node, expression, context) { node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); /** @type {Statement} */ - let statement = b.stmt( - (dynamic ? b.maybe_call : b.call)(expression, b.id('$$renderer'), props_expression) - ); + let statement = b.stmt(b.call(expression, b.id('$$renderer'), props_expression)); + + if (dynamic) { + statement = b.if( + expression, + b.block([ + b.stmt(b.call('$$renderer.push', block_open)), + statement, + b.stmt(b.call('$$renderer.push', block_close)) + ]), + b.block([ + b.stmt(b.call('$$renderer.push', block_open_else)), + b.stmt(b.call('$$renderer.push', block_close)) + ]) + ); + } if (snippet_declarations.length > 0) { statement = b.block([...snippet_declarations, statement]); @@ -326,16 +346,14 @@ export function build_inline_component(node, expression, context) { optimiser.check_blockers(node.metadata.expression); } - context.state.template.push( - ...optimiser.render_block([ - dynamic && custom_css_props.length === 0 - ? b.stmt(b.call('$$renderer.push', empty_comment)) - : b.empty, - statement - ]) - ); + context.state.template.push(...optimiser.render_block([statement])); - if (!optimiser.is_async() && !context.state.is_standalone && custom_css_props.length === 0) { + if ( + !dynamic && + !optimiser.is_async() && + !context.state.is_standalone && + custom_css_props.length === 0 + ) { context.state.template.push(empty_comment); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 134e57e627..0e9434386c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,8 +1,17 @@ /** @import { TemplateNode, Dom } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { block } from '../../reactivity/effects.js'; -import { hydrate_next, hydrating } from '../hydration.js'; +import { + hydrate_next, + hydrate_node, + hydrating, + read_hydration_instruction, + set_hydrate_node, + set_hydrating, + skip_nodes +} from '../hydration.js'; import { BranchManager } from './branches.js'; +import { HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js'; /** * @template P @@ -13,7 +22,11 @@ import { BranchManager } from './branches.js'; * @returns {void} */ export function component(node, get_component, render_fn) { + /** @type {TemplateNode | undefined} */ + var hydration_start_node; + if (hydrating) { + hydration_start_node = hydrate_node; hydrate_next(); } @@ -21,6 +34,28 @@ export function component(node, get_component, render_fn) { block(() => { var component = get_component() ?? null; + + if (hydrating) { + var data = read_hydration_instruction(/** @type {TemplateNode} */ (hydration_start_node)); + + var server_had_component = data === HYDRATION_START; + var client_has_component = component !== null; + + if (server_had_component !== client_has_component) { + // Hydration mismatch: skip the server-rendered nodes and render fresh + var anchor = skip_nodes(); + + set_hydrate_node(anchor); + branches.anchor = anchor; + + set_hydrating(false); + branches.ensure(component, component && ((target) => render_fn(target, component))); + set_hydrating(true); + + return; + } + } + branches.ensure(component, component && ((target) => render_fn(target, component))); }, EFFECT_TRANSPARENT); } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 0864c7704d..6ab32a0d6e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -105,16 +105,16 @@ export function css_props(renderer, is_html, props, component, dynamic = false) renderer.push(``); } - if (dynamic) { + component(); + + if (!dynamic) { renderer.push(''); } - component(); - if (is_html) { - renderer.push(``); + renderer.push(''); } else { - renderer.push(``); + renderer.push(''); } } diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte new file mode 100644 index 0000000000..57add17179 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/Component.svelte @@ -0,0 +1,7 @@ +
Hello
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js new file mode 100644 index 0000000000..3078392055 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + `
Hello
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte new file mode 100644 index 0000000000..80aec26eef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-css-props/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte new file mode 100644 index 0000000000..52ea02c559 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/HelloWorld.svelte @@ -0,0 +1 @@ +
Hello world
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js new file mode 100644 index 0000000000..90dae38ad8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

Test

Hello world
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte new file mode 100644 index 0000000000..8a410da609 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-falsy-hydrate/main.svelte @@ -0,0 +1,13 @@ + + +

Test

+ +