diff --git a/.changeset/swift-ravens-hunt.md b/.changeset/swift-ravens-hunt.md new file mode 100644 index 0000000000..d62ff1ca51 --- /dev/null +++ b/.changeset/swift-ravens-hunt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve template text node serialization diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 855a795284..5b4003f180 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1382,13 +1382,17 @@ function process_children(nodes, parent, { visit, state }) { /** * @param {Sequence} sequence + * @param {boolean} in_fragment */ - function flush_sequence(sequence) { + function flush_sequence(sequence, in_fragment) { if (sequence.length === 1) { const node = sequence[0]; - if (node.type === 'Text') { + if ((in_fragment && node.type === 'ExpressionTag') || node.type === 'Text') { expression = b.call('$.sibling', expression); + } + + if (node.type === 'Text') { state.template.push(node.raw); return; } @@ -1475,7 +1479,7 @@ function process_children(nodes, parent, { visit, state }) { sequence.push(node); } else { if (sequence.length > 0) { - flush_sequence(sequence); + flush_sequence(sequence, true); sequence = []; } @@ -1519,7 +1523,7 @@ function process_children(nodes, parent, { visit, state }) { } if (sequence.length > 0) { - flush_sequence(sequence); + flush_sequence(sequence, false); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 37f4cc757f..79153baed6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -173,7 +173,7 @@ function process_children(nodes, parent, { visit, state }) { } const expression = b.call( - '$.escape', + '$.escape_text', /** @type {import('estree').Expression} */ (visit(node.expression)) ); state.template.push(t_expression(expression)); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 4fe85992f4..dc0afe7ec2 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -150,6 +150,17 @@ export function escape(value, is_attr = false) { return escaped + str.substring(last); } +/** + * @template V + * @param {V} value + * @returns {string} + */ +export function escape_text(value) { + const escaped = escape(value); + // If the value is empty, then ensure we put a space so that it creates a text node on the client + return escaped === '' ? ' ' : escaped; +} + /** * @param {Payload} payload * @param {(head_payload: Payload['head']) => void} fn diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-template/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-template/_config.js new file mode 100644 index 0000000000..340b0ff4c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-template/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, component }) { + const [b1] = target.querySelectorAll('button'); + + flushSync(() => { + b1?.click(); + }); + + await Promise.resolve(); + assert.deepEqual(component.log, ['onclick']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-template/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-template/main.svelte new file mode 100644 index 0000000000..66db0b6013 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-template/main.svelte @@ -0,0 +1,10 @@ + + +{undefined}