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 <simon.holthausen@vercel.com>
svelte0/svelte-15053-fix-stop-currenttime-binding-updates-during-outros
svelte-triage-bot[bot] 1 month ago committed by GitHub
parent 7725f77cca
commit 221dcae8ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent declaration comments from breaking server derived references

@ -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<AST.Attribute | AST.SpreadAttribute>} */
/** @type {Array<AST.Attribute | AST.SpreadAttribute | TransformedAttribute>} */
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<AST.Attribute | TransformedAttribute>} */ (
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<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective | TransformedAttribute>} 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<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | TransformedAttribute>} 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<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */ (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<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective | TransformedAttribute>} attributes
* @param {AST.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context

@ -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;

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
ssrHtml: '<p>LATER</p> <input value="LATER">',
html: '<p>LATER</p> <input>'
});

@ -0,0 +1,14 @@
<script>
const ctx = {
get later() {
return later;
}
};
// a leading comment on the declaration
// that spans more than one line
let later = $derived.by(() => 'LATER');
</script>
<p>{ctx.later}</p>
<input bind:value={later} />
Loading…
Cancel
Save