From 59a96c9a9761d47e21b29183ac1d9d2bcea9b360 Mon Sep 17 00:00:00 2001 From: Jack Goodall Date: Mon, 28 Jul 2025 15:22:41 +0200 Subject: [PATCH] support spreading object with get/set methods --- .../client/visitors/BindDirective.js | 11 ++++-- .../client/visitors/shared/utils.js | 20 +---------- .../server/visitors/shared/element.js | 13 +++++-- .../3-transform/shared/spread_bindings.js | 36 +++++++++++++++++++ .../samples/bind-spread/_config.js | 8 +++-- .../samples/bind-spread/main.svelte | 15 +++++--- 6 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 56792d8dac..74b3978f5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -6,7 +6,8 @@ import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { binding_properties } from '../../../bindings.js'; import { build_attribute_value } from './shared/element.js'; -import { build_bind_this, validate_binding, handle_spread_binding } from './shared/utils.js'; +import { build_bind_this, validate_binding } from './shared/utils.js'; +import { handle_spread_binding } from '../../shared/spread_bindings.js'; /** * @param {AST.BindDirective} node @@ -14,10 +15,14 @@ import { build_bind_this, validate_binding, handle_spread_binding } from './shar */ export function BindDirective(node, context) { let get, set; - + // Handle SpreadElement by creating a variable declaration before visiting if (node.expression.type === 'SpreadElement') { - const { get: getter, set: setter } = handle_spread_binding(node.expression, context.state, context.visit); + const { get: getter, set: setter } = handle_spread_binding( + node.expression, + context.state, + context.visit + ); get = getter; set = setter; } else { 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 bac446cc0c..83d0a49959 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 @@ -9,6 +9,7 @@ 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 { handle_spread_binding } from '../../../shared/spread_bindings.js'; /** * A utility for extracting complex expressions (such as call expressions) @@ -202,25 +203,6 @@ export function parse_directive_name(name) { return expression; } -/** - * Handles SpreadElement by creating a variable declaration and returning getter/setter expressions - * @param {SpreadElement} spread_expression - * @param {ComponentClientTransformState} state - * @param {function} visit - * @returns {{get: Expression, set: Expression}} - */ -export function handle_spread_binding(spread_expression, state, visit) { - // Generate a unique variable name for this spread binding - const id = b.id(state.scope.generate('$$bindings')); - - const visited_expression = /** @type {Expression} */ (visit(spread_expression.argument)); - state.init.push(b.const(id, visited_expression)); - - const get = b.member(id, b.literal(0), true); - const set = b.member(id, b.literal(1), true); - return { get, set }; -} - /** * Serializes `bind:this` for components and elements. * @param {Identifier | MemberExpression | SequenceExpression | SpreadElement} expression 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 7207564ef9..0ce26571e5 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 @@ -21,6 +21,7 @@ import { is_load_error_element } from '../../../../../../utils.js'; import { escape_html } from '../../../../../../escaping.js'; +import { handle_spread_binding } from '../../../shared/spread_bindings.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; @@ -118,7 +119,11 @@ export function build_element_attributes(node, context) { let expression = /** @type {Expression} */ (context.visit(attribute.expression)); - if (expression.type === 'SequenceExpression') { + // Handle SpreadElement for bind directives + if (attribute.expression.type === 'SpreadElement') { + const { get } = handle_spread_binding(attribute.expression, context.state, context.visit); + expression = b.call(get); + } else if (expression.type === 'SequenceExpression') { expression = b.call(expression.expressions[0]); } @@ -126,7 +131,11 @@ export function build_element_attributes(node, context) { content = expression; } else if (attribute.name === 'value' && node.name === 'textarea') { content = b.call('$.escape', expression); - } else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') { + } else if ( + attribute.name === 'group' && + attribute.expression.type !== 'SequenceExpression' && + attribute.expression.type !== 'SpreadElement' + ) { const value_attribute = /** @type {AST.Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') ); diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js b/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js new file mode 100644 index 0000000000..01afe26b4b --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js @@ -0,0 +1,36 @@ +/** @import { Expression, MemberExpression, SequenceExpression, SpreadElement, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ +/** @import { ComponentClientTransformState } from '../client/types.js' */ +/** @import { ComponentServerTransformState } from '../server/types.js' */ +import * as b from '#compiler/builders'; + +/** + * Handles SpreadElement by creating a variable declaration and returning getter/setter expressions + * @param {SpreadElement} spread_expression + * @param {ComponentClientTransformState | ComponentServerTransformState} state + * @param {function} visit + * @returns {{get: Expression, set: Expression}} + */ +export function handle_spread_binding(spread_expression, state, visit) { + // Generate a unique variable name for this spread binding + const id = b.id(state.scope.generate('$$bindings')); + + const visited_expression = /** @type {Expression} */ (visit(spread_expression.argument)); + state.init.push(b.const(id, visited_expression)); + + // Create conditional expressions that work for both arrays and objects + // Array.isArray($$bindings) ? $$bindings[0] : $$bindings.get + const get = b.conditional( + b.call('Array.isArray', id), + b.member(id, b.literal(0), true), + b.member(id, b.id('get')) + ); + + // Array.isArray($$bindings) ? $$bindings[1] : $$bindings.set + const set = b.conditional( + b.call('Array.isArray', id), + b.member(id, b.literal(1), true), + b.member(id, b.id('set')) + ); + + return { get, set }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js index ae971acaf8..d2b13c9b19 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js @@ -10,14 +10,18 @@ export default test({ flushSync(); - assert.htmlEqual(target.innerHTML, ``.repeat(3)); + assert.htmlEqual(target.innerHTML, ``.repeat(4)); // assert.deepEqual(logs, ['b', '2', 'a', '2']); flushSync(() => { checkboxes.forEach((checkbox) => checkbox.click()); }); - assert.deepEqual(logs, ['getBindings', ...repeatArray(3, ['check', false])]); + assert.deepEqual(logs, [ + 'getArrayBindings', + 'getObjectBindings', + ...repeatArray(4, ['check', false]) + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte index 13646ef83a..d2cb8491e6 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte @@ -9,10 +9,16 @@ } ]; - function getBindings() { - console.log('getBindings'); + function getArrayBindings() { + console.log('getArrayBindings'); return check_bindings; } + + function getObjectBindings() { + console.log('getObjectBindings'); + const [get, set] = check_bindings; + return { get, set }; + } @@ -20,5 +26,6 @@ - - + + +