From 221dcae8ca50c84c99c9bf5475e91dc3aa25480e Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:34:22 +0200 Subject: [PATCH] fix: preserve comment placement for server derived reads (#18641) Fixes https://github.com/sveltejs/svelte/issues/18607. For server output, `build_getter` reused a derived binding's declaration identifier as the generated call callee. Esrap therefore associated leading declaration comments with an earlier reference, potentially emitting them immediately after `return` and triggering automatic semicolon insertion. The getter returned `undefined` instead of the derived value. Fix it by avoiding the double-transform. --------- Co-authored-by: svelte-triage-bot[bot] <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/tidy-cats-return.md | 5 + .../server/visitors/shared/element.js | 91 ++++++++++--------- .../server/visitors/shared/utils.js | 2 +- .../_config.js | 6 ++ .../main.svelte | 14 +++ 5 files changed, 74 insertions(+), 44 deletions(-) create mode 100644 .changeset/tidy-cats-return.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/main.svelte diff --git a/.changeset/tidy-cats-return.md b/.changeset/tidy-cats-return.md new file mode 100644 index 0000000000..0c041196ef --- /dev/null +++ b/.changeset/tidy-cats-return.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent declaration comments from breaking server derived references diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 9c37b2fffe..6440510b01 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -3,7 +3,7 @@ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; -import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js'; +import { ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js'; import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '#compiler/builders'; import { @@ -21,6 +21,11 @@ import { escape_html } from '../../../../../../escaping.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; +/** + * @typedef {{ type: 'transformed', name: string, expression: Expression }} TransformedAttribute + * An attribute whose expression has already been transformed and must not be visited again. + */ + /** * Writes the output to the template output. Some elements may have attributes on them that require the * their output to be the child content instead. In this case, an object is returned. @@ -29,7 +34,7 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ export function build_element_attributes(node, context, transform) { - /** @type {Array} */ + /** @type {Array} */ const attributes = []; /** @type {AST.ClassDirective[]} */ @@ -145,42 +150,26 @@ export function build_element_attributes(node, context, transform) { attr.value[0].data === 'checkbox' ); - attributes.push( - create_attribute('checked', null, -1, -1, [ - { - type: 'ExpressionTag', - start: -1, - end: -1, - expression: is_checkbox - ? b.call( - b.member(attribute.expression, 'includes'), - build_attribute_value(value_attribute.value, context, transform) - ) - : b.binary( - '===', - attribute.expression, - build_attribute_value(value_attribute.value, context, transform) - ), - metadata: { - expression: new ExpressionMetadata() - } - } - ]) - ); + attributes.push({ + type: 'transformed', + name: 'checked', + expression: is_checkbox + ? b.call( + b.member(expression, 'includes'), + build_attribute_value(value_attribute.value, context, transform) + ) + : b.binary( + '===', + expression, + build_attribute_value(value_attribute.value, context, transform) + ) + }); } else { - attributes.push( - create_attribute(attribute.name, null, -1, -1, [ - { - type: 'ExpressionTag', - start: -1, - end: -1, - expression, - metadata: { - expression: new ExpressionMetadata() - } - } - ]) - ); + attributes.push({ + type: 'transformed', + name: get_attribute_name(node, attribute), + expression + }); } } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); @@ -217,7 +206,21 @@ export function build_element_attributes(node, context, transform) { } else { const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; - for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { + for (const attribute of /** @type {Array} */ ( + attributes + )) { + if (attribute.type === 'transformed') { + context.state.template.push( + b.call( + '$.attr', + b.literal(attribute.name), + attribute.expression, + is_boolean_attribute(attribute.name) && b.true + ) + ); + continue; + } + const name = get_attribute_name(node, attribute); const can_use_literal = (name !== 'class' || class_directives.length === 0) && @@ -298,14 +301,16 @@ function get_attribute_name(element, attribute) { /** * @param {AST.RegularElement | AST.SvelteElement} element - * @param {Array} attributes + * @param {Array} attributes * @param {ComponentContext} context * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ export function build_spread_object(element, attributes, context, transform) { const object = b.object( attributes.map((attribute) => { - if (attribute.type === 'Attribute') { + if (attribute.type === 'transformed') { + return b.prop('init', b.key(attribute.name), attribute.expression); + } else if (attribute.type === 'Attribute') { const name = get_attribute_name(element, attribute); const value = build_attribute_value( attribute.value, @@ -340,7 +345,7 @@ export function build_spread_object(element, attributes, context, transform) { /** * * @param {AST.RegularElement | AST.SvelteElement} element - * @param {Array} attributes + * @param {Array} attributes * @param {AST.StyleDirective[]} style_directives * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context @@ -356,7 +361,7 @@ function build_element_spread_attributes( ) { const args = prepare_element_spread( element, - /** @type {Array} */ (attributes), + attributes, style_directives, class_directives, context, @@ -410,7 +415,7 @@ export function prepare_element_spread_object(element, context, transform) { /** * Prepare args for $.attributes(...): compute object, css_hash, classes, styles and flags. * @param {AST.RegularElement | AST.SvelteElement} element - * @param {Array} attributes + * @param {Array} attributes * @param {AST.StyleDirective[]} style_directives * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context 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 56157cee37..28d2d762f9 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 @@ -286,7 +286,7 @@ export function build_getter(node, state) { } if (binding.kind === 'derived') { - return (binding.declaration_kind === 'var' ? b.maybe_call : b.call)(binding.node); + return (binding.declaration_kind === 'var' ? b.maybe_call : b.call)(node); } return node; diff --git a/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/_config.js new file mode 100644 index 0000000000..204581389d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: '

LATER

', + html: '

LATER

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/main.svelte new file mode 100644 index 0000000000..f9a1a1a6fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-forward-ref-leading-comments/main.svelte @@ -0,0 +1,14 @@ + + +

{ctx.later}

+