diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 9651d956ba..810513e4f9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -818,9 +818,8 @@ function read_sequence(parser, done, location) { parser.allow_whitespace(); - const has_spread = parser.match('...'); + const has_spread = parser.eat('...'); if (has_spread) { - parser.eat('...', true); parser.allow_whitespace(); } 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 a562a0d46d..f0efc56f96 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -253,7 +253,7 @@ export function BindDirective(node, context) { node.metadata = { binding_group_name: group_name, parent_each_blocks: each_blocks, - spread_binding: false + spread_binding: node.metadata.spread_binding }; } 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 5e678f4332..74e5f1c989 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 @@ -197,6 +197,35 @@ export function build_component(node, component_name, context) { push_prop(b.init(attribute.name, value)); } } else if (attribute.type === 'BindDirective') { + const expression = /** @type {Expression} */ (context.visit(attribute.expression)); + + if ( + dev && + attribute.name !== 'this' && + !is_ignored(node, 'ownership_invalid_binding') && + // bind:x={() => x.y, y => x.y = y} and bind:x={...[() => x.y, y => x.y = y]} + // will be handled by the assignment expression binding validation + attribute.expression.type !== 'SequenceExpression' && + !attribute.metadata.spread_binding + ) { + const left = object(attribute.expression); + const binding = left && context.state.scope.get(left.name); + + if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') { + context.state.analysis.needs_mutation_validation = true; + binding_initializers.push( + b.stmt( + b.call( + '$$ownership_validator.binding', + b.literal(binding.node.name), + b.id(is_component_dynamic ? intermediate_name : component_name), + b.thunk(expression) + ) + ) + ); + } + } + if (attribute.metadata.spread_binding) { const { get, set } = init_spread_bindings(attribute.expression, context); @@ -209,91 +238,57 @@ export function build_component(node, component_name, context) { push_prop(b.get(attribute.name, [b.return(b.call(get))]), true); push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true); } - } else { - const expression = /** @type {Expression} */ (context.visit(attribute.expression)); + } else if (expression.type === 'SequenceExpression') { + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const [get, set] = expression.expressions; + 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 && - attribute.name !== 'this' && - !is_ignored(node, 'ownership_invalid_binding') && - // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation - attribute.expression.type !== 'SequenceExpression' + expression.type === 'MemberExpression' && + context.state.analysis.runes && + !is_ignored(node, 'binding_property_non_reactive') ) { - const left = object(attribute.expression); - const binding = left && context.state.scope.get(left.name); - - if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') { - context.state.analysis.needs_mutation_validation = true; - binding_initializers.push( - b.stmt( - b.call( - '$$ownership_validator.binding', - b.literal(binding.node.name), - b.id(is_component_dynamic ? intermediate_name : component_name), - b.thunk(expression) - ) - ) - ); - } + validate_binding(context.state, attribute, expression); } - if (expression.type === 'SequenceExpression') { - if (attribute.name === 'this') { - bind_this = attribute.expression; - } else { - const [get, set] = expression.expressions; - 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')))])); - } + if (attribute.name === 'this') { + bind_this = attribute.expression; } else { - 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; - } else { - const is_store_sub = - attribute.expression.type === 'Identifier' && - context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; - - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - if (is_store_sub) { - push_prop( - b.get(attribute.name, [ - b.stmt(b.call('$.mark_store_binding')), - b.return(expression) - ]), - true - ); - } else { - push_prop(b.get(attribute.name, [b.return(expression)]), true); - } - - const assignment = b.assignment( - '=', - /** @type {Pattern} */ (attribute.expression), - b.id('$$value') - ); + const is_store_sub = + attribute.expression.type === 'Identifier' && + context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + if (is_store_sub) { push_prop( - b.set(attribute.name, [ - b.stmt(/** @type {Expression} */ (context.visit(assignment))) - ]), + b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), true ); + } else { + push_prop(b.get(attribute.name, [b.return(expression)]), true); } + + const assignment = b.assignment( + '=', + /** @type {Pattern} */ (attribute.expression), + b.id('$$value') + ); + + push_prop( + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), + true + ); } } } else if (attribute.type === 'AttachTag') { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index a48a7c15e7..6c8701d6b9 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -15,8 +15,7 @@ import type { Program, ChainExpression, SimpleCallExpression, - SequenceExpression, - SpreadElement + SequenceExpression } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css';