diff --git a/.changeset/little-needles-itch.md b/.changeset/little-needles-itch.md new file mode 100644 index 000000000000..1f83e77e8763 --- /dev/null +++ b/.changeset/little-needles-itch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: adjust template string concat strategy diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 4008dbdfa3cb..a63fbc9b7185 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -74,8 +74,23 @@ export function process_children(nodes, initial, is_element, { visit, state }) { // no text node was created because the expression was empty during SSR const is_text = sequence.length === 1; const id = flush_node(is_text, 'text'); + const parts = []; + + if (state.analysis.runes && value.type === 'TemplateLiteral') { + for (let i = 0; i < value.quasis.length; i++) { + const quasi = value.quasis[i]; + parts.push(b.literal(/** @type {string} */ (quasi.value.cooked))); + if (i !== value.quasis.length - 1) { + const expression = value.expressions[i]; + parts.push(expression); + } + } + } - const update = b.stmt(b.call('$.set_text', id, value)); + const update = + parts.length > 0 + ? b.stmt(b.call('$.set_text_parts', id, ...parts)) + : b.stmt(b.call('$.set_text', id, value)); if (has_call && !within_bound_contenteditable) { state.init.push(build_update(update)); diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 76e5ca3cb2a4..a403e67f3e3a 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -48,6 +48,8 @@ export function init_operations() { // @ts-expect-error Text.prototype.__t = undefined; + // @ts-expect-error + Text.prototype.__p = undefined; if (DEV) { // @ts-expect-error diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 306fc69ca745..aef348568de6 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -123,7 +123,7 @@ export { update_pre_store, update_store } from './reactivity/store.js'; -export { set_text } from './render.js'; +export { set_text, set_text_parts } from './render.js'; export { get, safe_get, diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index de986ae04e08..187a648c9aa6 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -9,7 +9,7 @@ import { init_operations } from './dom/operations.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; -import { push, pop, component_context, active_effect } from './runtime.js'; +import { push, pop, component_context, active_effect, untrack } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_next, @@ -56,6 +56,31 @@ export function set_text(text, value) { } } +/** + * @param {Element} text + * @param {any[]} parts + * @returns {void} + */ +export function set_text_parts(text, ...parts) { + var value = ''; + // @ts-expect-error + var prev_parts = text.__p || []; + var changed = false; + for (var i = 0; i < parts.length; i++) { + var prev = prev_parts[i]; + var next = parts[i]; + if (prev !== next) { + changed = true; + } + value += next; + } + // @ts-expect-error + text.__p = parts; + if (changed) { + set_text(text, value); + } +} + /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. * Transitions will play during the initial render unless the `intro` option is set to `false`. diff --git a/packages/svelte/tests/migrate/samples/self-closing-elements/output.svelte b/packages/svelte/tests/migrate/samples/self-closing-elements/output.svelte index 6aee688bb804..6dc21840d1bb 100644 --- a/packages/svelte/tests/migrate/samples/self-closing-elements/output.svelte +++ b/packages/svelte/tests/migrate/samples/self-closing-elements/output.svelte @@ -2,4 +2,4 @@

- + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte index 1071d37c39c6..14e9e7acc75f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte @@ -18,4 +18,4 @@ -{state.data.list} +{state.data.list.toString()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js new file mode 100644 index 000000000000..684e5870c19d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + `, + + ssrHtml: ` + + `, + + test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte new file mode 100644 index 000000000000..a02ab881c6e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 58ccf7ed7fda..e59aeee6abff 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -29,6 +29,6 @@ export default function Bind_component_snippet($$anchor) { var text_1 = $.sibling(node); - $.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ""}`)); + $.template_effect(() => $.set_text_parts(text_1, " value: ", $.get(value) ?? "", "")); $.append($$anchor, fragment); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 81c06f23ec3a..5b6226e5476b 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -15,4 +15,4 @@ export default function Each_string_template($$anchor) { }); $.append($$anchor, fragment); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 95d1c72017d3..17df0343ccbb 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -19,7 +19,7 @@ export default function Function_prop_no_getter($$anchor) { var text = $.text(); - $.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`)); + $.template_effect(() => $.set_text_parts(text, "clicks: ", $.get(count) ?? "", "")); $.append($$anchor, text); }, $$slots: { default: true } diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index 8ba14526aaf9..2287eb232fb9 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -20,7 +20,7 @@ export default function Text_nodes_deriveds($$anchor) { const stringified_text_1 = $.derived(() => text2() ?? ""); var text = $.child(p); - $.template_effect(() => $.set_text(text, `${$.get(stringified_text)}${$.get(stringified_text_1)}`)); + $.template_effect(() => $.set_text_parts(text, "", $.get(stringified_text), "", $.get(stringified_text_1), "")); $.reset(p); $.append($$anchor, p); } \ No newline at end of file