From b9e03714ebe1dbad9b31351f4e2d5487c90c670a Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 2 Jul 2026 08:30:23 +0200 Subject: [PATCH 1/3] fix: correctly transform declaration tags during SSR (#18492) Closes #18490 --- .changeset/rich-jokes-attack.md | 5 +++++ .../phases/3-transform/server/visitors/shared/utils.js | 6 ++++-- .../samples/declaration-tags-transform/_expected.html | 1 + .../samples/declaration-tags-transform/main.svelte | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/rich-jokes-attack.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte diff --git a/.changeset/rich-jokes-attack.md b/.changeset/rich-jokes-attack.md new file mode 100644 index 0000000000..46e7d2d8c8 --- /dev/null +++ b/.changeset/rich-jokes-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform declaration tags during SSR diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 3da6fcf321..56157cee37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -60,7 +60,9 @@ export function process_children(nodes, { visit, state }) { if (evaluated.is_known) { quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + expressions.push( + b.call('$.escape', /** @type {Expression} */ (visit(node.expression, state))) + ); quasi = b.quasi('', i + 1 === sequence.length); quasis.push(quasi); @@ -80,7 +82,7 @@ export function process_children(nodes, { visit, state }) { if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) { flush(); - const expression = /** @type {Expression} */ (visit(node.expression)); + const expression = /** @type {Expression} */ (visit(node.expression, state)); let call = b.call( '$$renderer.push', diff --git a/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html new file mode 100644 index 0000000000..5665318328 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html @@ -0,0 +1 @@ +
number
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte new file mode 100644 index 0000000000..d969f907a5 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte @@ -0,0 +1,4 @@ +
+ {let dt = $derived(Date.parse("2026-10-01T00:00:00Z"))} + {typeof dt} +
\ No newline at end of file From b1cadd1eae6a709fc5bdc596256b617986a71aaf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 2 Jul 2026 16:31:54 +0200 Subject: [PATCH 2/3] chore: drop dead code that make TSGO fail (#18496) --- .changeset/big-months-shout.md | 5 +++++ packages/svelte/src/internal/client/reactivity/props.js | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .changeset/big-months-shout.md diff --git a/.changeset/big-months-shout.md b/.changeset/big-months-shout.md new file mode 100644 index 0000000000..3d8ca0045e --- /dev/null +++ b/.changeset/big-months-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: drop dead code that make TSGO fail diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index c2f3698809..274d780b1e 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -91,10 +91,7 @@ const rest_props_handler = { */ /*#__NO_SIDE_EFFECTS__*/ export function rest_props(props, exclude, name) { - return new Proxy( - DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, - rest_props_handler - ); + return new Proxy(DEV ? { props, exclude, name } : { props, exclude }, rest_props_handler); } /** From 8fb7ceeba5dd1d3b7fc3f587f4e2138bf1b978ed Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 6 Jul 2026 13:50:56 +0200 Subject: [PATCH 3/3] fix: don't treat declaration tags as parts inside each blocks (#18507) Closes #18506 Declaration tags in the scope of an each block were considered "parts" and transformed to use their value instead of the signal itself. We can safely check on the `kind` of the binding since a `kind` of `state`, `state_raw` or `derived` in the each scope needs to be a declaration tag (and it's a stable reference so we can use them directly) --- .changeset/young-doodles-beam.md | 5 +++++ .../phases/3-transform/client/visitors/shared/utils.js | 6 +++++- .../samples/declaration-tags-each/_config.js | 10 ++++++++++ .../samples/declaration-tags-each/main.svelte | 6 ++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/young-doodles-beam.md create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte diff --git a/.changeset/young-doodles-beam.md b/.changeset/young-doodles-beam.md new file mode 100644 index 0000000000..b68f55d241 --- /dev/null +++ b/.changeset/young-doodles-beam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't treat declaration tags as parts inside each blocks diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 51c84dd01c..c305d53ea5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; -import { build_getter } from '../../utils.js'; +import { build_getter, is_state_source } from '../../utils.js'; import { ExpressionMetadata } from '../../../../nodes.js'; /** @@ -272,6 +272,10 @@ export function build_bind_this(expression, value, { state, visit }) { const binding = state.scope.get(node.name); if (!binding) return; + // if it is a state or a derived it means is a declaration tag...in that case we don't want to pass the + // value but the signal itself or assignment will break + if (is_state_source(binding, state.analysis) || binding.kind === 'derived') return; + for (const [owner, scope] of state.scopes) { if (owner.type === 'EachBlock' && scope === binding.scope) { ids.push(node); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js new file mode 100644 index 0000000000..d5cdd103b5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js @@ -0,0 +1,10 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + const [btn1, btn2] = target.querySelectorAll('button'); + flushSync(() => btn2.click()); + assert.equal(btn1.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte new file mode 100644 index 0000000000..04d55913b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte @@ -0,0 +1,6 @@ +{#each Array(1)} + {let ref = $state()} + {let count = $state(0)} + + +{/each} \ No newline at end of file