diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 5b56d9ddac..e9796737f0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -20,6 +20,14 @@ export function BindDirective(node, context) { validate_no_const_assignment(node, node.expression, context.state.scope, true); const assignee = node.expression; + + if (assignee.type === 'SequenceExpression') { + if (assignee.expressions.length !== 2) { + e.bind_invalid_expression(node); + } + return; + } + const left = object(assignee); if (left === null) { 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 79969240c7..43e0c0029a 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 @@ -36,18 +36,26 @@ export function BindDirective(node, context) { ); } - const get = b.thunk(/** @type {Expression} */ (context.visit(expression))); - - /** @type {Expression | undefined} */ - let set = b.unthunk( - b.arrow( - [b.id('$$value')], - /** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) - ) - ); - - if (get === set) { - set = undefined; + let get, set; + + if (expression.type === 'SequenceExpression') { + const [get_expression, set_expression] = expression.expressions; + get = /** @type {Expression} */ (context.visit(get_expression)); + set = /** @type {Expression} */ (context.visit(set_expression)); + } else { + get = b.thunk(/** @type {Expression} */ (context.visit(expression))); + + /** @type {Expression | undefined} */ + set = b.unthunk( + b.arrow( + [b.id('$$value')], + /** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) + ) + ); + + if (get === set) { + set = undefined; + } } /** @type {CallExpression} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 4d3cebcee6..5e88ca1b98 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) { if (context.state.analysis.runes) return; let bound = value_binding.expression; + + if (bound.type === 'SequenceExpression') { + return; + } + while (bound.type === 'MemberExpression') { bound = /** @type {Identifier | MemberExpression} */ (bound.object); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 8e1a536707..34875a6ba4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -161,49 +161,63 @@ export function build_component(node, component_name, context, anchor = context. push_prop(b.init(attribute.name, value)); } } else if (attribute.type === 'BindDirective') { - const expression = /** @type {Expression} */ (context.visit(attribute.expression)); - - if ( - dev && - expression.type === 'MemberExpression' && - context.state.analysis.runes && - !is_ignored(node, 'binding_property_non_reactive') - ) { - validate_binding(context.state, attribute, expression); - } - - if (attribute.name === 'this') { - bind_this = attribute.expression; + if (attribute.expression.type === 'SequenceExpression') { + const [get_expression, set_expression] = attribute.expression.expressions; + const get = /** @type {Expression} */ (context.visit(get_expression)); + const set = /** @type {Expression} */ (context.visit(set_expression)); + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); } else { - if (dev) { - binding_initializers.push( - b.stmt( - b.call( - b.id('$.add_owner_effect'), - b.thunk(expression), - b.id(component_name), - is_ignored(node, 'ownership_invalid_binding') && b.true - ) - ) - ); + const expression = /** @type {Expression} */ (context.visit(attribute.expression)); + + if ( + dev && + expression.type === 'MemberExpression' && + context.state.analysis.runes && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, attribute, expression); } - const is_store_sub = - attribute.expression.type === 'Identifier' && - context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; - - if (is_store_sub) { + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + if (dev) { + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + b.thunk(expression), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) + ) + ); + } + + const is_store_sub = + attribute.expression.type === 'Identifier' && + context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + + if (is_store_sub) { + push_prop( + b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]) + ); + } else { + push_prop(b.get(attribute.name, [b.return(expression)])); + } + + const assignment = b.assignment('=', attribute.expression, b.id('$$value')); push_prop( - b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]) + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]) ); - } else { - push_prop(b.get(attribute.name, [b.return(expression)])); } - - const assignment = b.assignment('=', attribute.expression, b.id('$$value')); - push_prop( - b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]) - ); } } } 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 21b902d5c2..d66475e9aa 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 @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { AST, SvelteNode } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; @@ -157,11 +157,19 @@ export function build_update_assignment(state, id, init, value, update) { /** * Serializes `bind:this` for components and elements. - * @param {Identifier | MemberExpression} expression + * @param {Identifier | MemberExpression | SequenceExpression} expression * @param {Expression} value * @param {import('zimmerframe').Context} context */ export function build_bind_this(expression, value, { state, visit }) { + if (expression.type === 'SequenceExpression') { + const [get_expression, set_expression] = expression.expressions; + const get = /** @type {Expression} */ (visit(get_expression)); + const set = /** @type {Expression} */ (visit(set_expression)); + + return b.call('$.bind_this', value, get, set); + } + /** @type {Identifier[]} */ const ids = []; @@ -238,6 +246,9 @@ export function build_bind_this(expression, value, { state, visit }) { * @param {MemberExpression} expression */ export function validate_binding(state, binding, expression) { + if (binding.expression.type === 'SequenceExpression') { + return; + } // If we are referencing a $store.foo then we don't need to add validation const left = object(binding.expression); const left_binding = left && state.scope.get(left.name); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 79df3cdd04..6f73c53284 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -81,22 +81,36 @@ export function build_inline_component(node, expression, context) { const value = build_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child - push_prop( - b.get(attribute.name, [ - b.return(/** @type {Expression} */ (context.visit(attribute.expression))) - ]) - ); - push_prop( - b.set(attribute.name, [ - b.stmt( - /** @type {Expression} */ ( - context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) - ) - ), - b.stmt(b.assignment('=', b.id('$$settled'), b.false)) - ]) - ); + if (attribute.expression.type === 'SequenceExpression') { + const [get_expression, set_expression] = attribute.expression.expressions; + const get = /** @type {Expression} */ (context.visit(get_expression)); + const set = /** @type {Expression} */ (context.visit(set_expression)); + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } else { + // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child + push_prop( + b.get(attribute.name, [ + b.return(/** @type {Expression} */ (context.visit(attribute.expression))) + ]) + ); + push_prop( + b.set(attribute.name, [ + b.stmt( + /** @type {Expression} */ ( + context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) + ) + ), + b.stmt(b.assignment('=', b.id('$$settled'), b.false)) + ]) + ); + } } } 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 c386c4f7c0..5999f0b0b6 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 @@ -110,11 +110,16 @@ export function build_element_attributes(node, context) { if (binding?.omit_in_ssr) continue; if (is_content_editable_binding(attribute.name)) { - content = /** @type {Expression} */ (context.visit(attribute.expression)); + content = + attribute.expression.type === 'SequenceExpression' + ? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0]))) + : /** @type {Expression} */ (context.visit(attribute.expression)); } else if (attribute.name === 'value' && node.name === 'textarea') { content = b.call( '$.escape', - /** @type {Expression} */ (context.visit(attribute.expression)) + attribute.expression.type === 'SequenceExpression' + ? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0]))) + : /** @type {Expression} */ (context.visit(attribute.expression)) ); } else if (attribute.name === 'group') { const value_attribute = /** @type {AST.Attribute | undefined} */ ( @@ -129,6 +134,11 @@ export function build_element_attributes(node, context) { is_text_attribute(attr) && attr.value[0].data === 'checkbox' ); + const attribute_expression = + attribute.expression.type === 'SequenceExpression' + ? b.call(attribute.expression.expressions[0]) + : attribute.expression; + attributes.push( create_attribute('checked', -1, -1, [ { @@ -138,12 +148,12 @@ export function build_element_attributes(node, context) { parent: attribute, expression: is_checkbox ? b.call( - b.member(attribute.expression, 'includes'), + b.member(attribute_expression, 'includes'), build_attribute_value(value_attribute.value, context) ) : b.binary( '===', - attribute.expression, + attribute_expression, build_attribute_value(value_attribute.value, context) ), metadata: { @@ -153,6 +163,11 @@ export function build_element_attributes(node, context) { ]) ); } else { + const attribute_expression = + attribute.expression.type === 'SequenceExpression' + ? b.call(attribute.expression.expressions[0]) + : attribute.expression; + attributes.push( create_attribute(attribute.name, -1, -1, [ { @@ -160,7 +175,7 @@ export function build_element_attributes(node, context) { start: -1, end: -1, parent: attribute, - expression: attribute.expression, + expression: attribute_expression, metadata: { expression: create_expression_metadata() } diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 2bd5fbbfa6..0013f5c17a 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -6,7 +6,8 @@ import type { Identifier, MemberExpression, ObjectExpression, - Pattern + Pattern, + SequenceExpression } from 'estree'; interface BaseNode { @@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } export interface LegacyBody extends BaseElement { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index fd1824d3b3..cc153b263a 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -14,7 +14,8 @@ import type { Pattern, Program, ChainExpression, - SimpleCallExpression + SimpleCallExpression, + SequenceExpression } from 'estree'; import type { Scope } from '../phases/scope'; @@ -185,7 +186,7 @@ export namespace AST { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; /** @internal */ metadata: { binding_group_name: Identifier; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 00ba2556d9..3079071fef 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -606,7 +606,7 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression } from 'estree'; + import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; import type { Location } from 'locate-character'; /** @@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } /** A `class:` directive */