From 3a462ce35c901bc007115edda6e3045a015e1849 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 29 Jan 2026 14:34:09 -0500 Subject: [PATCH] WIP --- .../compiler/phases/1-parse/state/element.js | 47 ++++++++++++------- .../src/compiler/phases/2-analyze/index.js | 2 +- .../2-analyze/visitors/BindDirective.js | 2 +- .../client/visitors/BindDirective.js | 14 ++++-- .../client/visitors/RegularElement.js | 2 +- .../client/visitors/shared/component.js | 8 ++-- .../client/visitors/shared/utils.js | 5 +- .../server/visitors/shared/component.js | 4 +- .../server/visitors/shared/element.js | 14 ++++-- packages/svelte/src/compiler/phases/nodes.js | 3 -- .../svelte/src/compiler/types/template.d.ts | 6 +-- 11 files changed, 64 insertions(+), 43 deletions(-) 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 b6e85db4f1..bff06b5dac 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -608,8 +608,33 @@ function read_attribute(parser) { } ]; end = parser.index; + } else if (type === 'BindDirective') { + // allow quote marks for legacy reasons. TODO 6.0 disallow + const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null; + parser.eat('{', true); + const spread = parser.eat('...'); + const expression = spread ? b.spread(read_expression(parser)) : read_expression(parser); + parser.allow_whitespace(); + parser.eat('}', true); + if (quote_mark) parser.eat(quote_mark, true); + end = parser.index; + + return { + start, + end, + type, + name: tag.name.slice(5), + modifiers: [], + name_loc: tag.loc, + // @ts-ignore + expression, + // @ts-ignore + metadata: { + expression: new ExpressionMetadata() + } + }; } else { - value = read_attribute_value(parser, type); + value = read_attribute_value(parser); end = parser.index; } } else if (parser.match_regex(regex_starts_with_quote_characters)) { @@ -667,13 +692,6 @@ function read_attribute(parser) { } }); - if (first_value?.metadata.expression.has_spread) { - if (directive.type !== 'BindDirective') { - e.directive_invalid_value(first_value.start); - } - directive.metadata.spread_binding = true; - } - // @ts-expect-error we do this separately from the declaration to avoid upsetting typescript directive.modifiers = modifiers; @@ -718,10 +736,9 @@ function get_directive_type(name) { /** * @param {Parser} parser - * @param {AST.AttributeLike['type'] | undefined} type * @return {AST.ExpressionTag | Array} */ -function read_attribute_value(parser, type) { +function read_attribute_value(parser) { const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null; if (quote_mark && parser.eat(quote_mark)) { return [ @@ -745,8 +762,7 @@ function read_attribute_value(parser, type) { if (quote_mark) return parser.match(quote_mark); return !!parser.match_regex(regex_invalid_unquoted_attribute_value); }, - 'in attribute value', - type === 'BindDirective' + 'in attribute value' ); } catch (/** @type {any} */ error) { if (error.code === 'js_parse_error') { @@ -779,10 +795,9 @@ function read_attribute_value(parser, type) { * @param {Parser} parser * @param {() => boolean} done * @param {string} location - * @param {boolean} allow_spread * @returns {any[]} */ -function read_sequence(parser, done, location, allow_spread = false) { +function read_sequence(parser, done, location) { /** @type {AST.Text} */ let current_chunk = { start: parser.index, @@ -827,8 +842,6 @@ function read_sequence(parser, done, location, allow_spread = false) { parser.allow_whitespace(); - const has_spread = allow_spread && parser.eat('...'); - const expression = read_expression(parser); parser.allow_whitespace(); parser.eat('}', true); @@ -844,8 +857,6 @@ function read_sequence(parser, done, location, allow_spread = false) { } }; - chunk.metadata.expression.has_spread = has_spread; - chunks.push(chunk); current_chunk = { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 55f585293b..3939c89fbc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -738,7 +738,7 @@ export function analyze_component(root, source, options) { type === 'FunctionExpression' || type === 'ArrowFunctionExpression' || (type === 'BindDirective' && - /** @type {AST.BindDirective} */ (path[i]).metadata.spread_binding) + /** @type {AST.BindDirective} */ (path[i]).expression.type === 'SpreadElement') ) { continue inner; } 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 828369436e..e49d7522c6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -178,7 +178,7 @@ export function BindDirective(node, context) { return; } - if (node.metadata.spread_binding) { + if (node.expression.type === 'SpreadElement') { if (node.name === 'group') { e.bind_group_invalid_expression(node); } 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 5dee2da154..74e7362f69 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 @@ -1,4 +1,4 @@ -/** @import { CallExpression, Expression, Pattern } from 'estree' */ +/** @import { CallExpression, Expression, Pattern, SpreadElement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; @@ -14,15 +14,19 @@ import { init_spread_bindings } from '../../shared/spread_bindings.js'; * @param {ComponentContext} context */ export function BindDirective(node, context) { - const expression = /** @type {Expression} */ (context.visit(node.expression)); - const property = binding_properties[node.name]; + const expression = /** @type {Expression} */ ( + context.visit( + node.expression.type === 'SpreadElement' ? node.expression.argument : node.expression + ) + ); + const property = binding_properties[node.name]; const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1)); let get, set; - if (node.metadata.spread_binding) { - [get, set] = init_spread_bindings(node.expression, context); + if (node.expression.type === 'SpreadElement') { + [get, set] = init_spread_bindings(expression, context); } else if (expression.type === 'SequenceExpression') { [get, set] = expression.expressions; } else { 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 556778d218..783fc6f30b 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 @@ -515,7 +515,7 @@ function setup_select_synchronization(value_binding, context) { let bound = value_binding.expression; - if (bound.type === 'SequenceExpression' || value_binding.metadata.spread_binding) { + if (bound.type === 'SequenceExpression' || bound.type === 'SpreadElement') { return; } 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 d95c3fc724..f028fd25ae 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 @@ -212,7 +212,7 @@ export function build_component(node, component_name, loc, context) { // 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 + attribute.expression.type !== 'SpreadElement' ) { const left = object(attribute.expression); const binding = left && context.state.scope.get(left.name); @@ -232,13 +232,13 @@ export function build_component(node, component_name, loc, context) { } } - if (attribute.metadata.spread_binding) { - const [get, set] = init_spread_bindings(attribute.expression, context); + if (attribute.expression.type === 'SpreadElement') { + const [get, set] = init_spread_bindings(attribute.expression.argument, context); if (attribute.name === 'this') { bind_this = { type: 'SpreadElement', - argument: attribute.expression + argument: attribute.expression.argument }; } else { push_prop(b.get(attribute.name, [b.return(b.call(get))]), true); 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 8a1c0d657d..1d0c7c93f2 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 @@ -346,7 +346,10 @@ export function build_bind_this(expression, value, context) { * @param {MemberExpression} expression */ export function validate_binding(state, binding, expression) { - if (binding.expression.type === 'SequenceExpression' || binding.metadata.spread_binding) { + if ( + binding.expression.type === 'SequenceExpression' || + binding.expression.type === 'SpreadElement' + ) { return; } // If we are referencing a $store.foo then we don't need to add validation 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 24096e0355..c281280335 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 @@ -111,8 +111,8 @@ export function build_inline_component(node, expression, context) { // Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers optimiser.check_blockers(attribute.metadata.expression); - if (attribute.metadata.spread_binding) { - const [get, set] = init_spread_bindings(attribute.expression, context); + if (attribute.expression.type === 'SpreadElement') { + const [get, set] = init_spread_bindings(attribute.expression.argument, context); push_prop(b.get(attribute.name, [b.return(b.call(get))])); push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))])); 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 7bc32982b7..cba205f346 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 @@ -116,10 +116,16 @@ export function build_element_attributes(node, context, transform) { const binding = binding_properties[attribute.name]; if (binding?.omit_in_ssr) continue; - let expression = /** @type {Expression} */ (context.visit(attribute.expression)); + let expression = /** @type {Expression} */ ( + context.visit( + attribute.expression.type === 'SpreadElement' + ? attribute.expression.argument + : attribute.expression + ) + ); - if (attribute.metadata.spread_binding) { - const [get] = init_spread_bindings(attribute.expression, context); + if (attribute.expression.type === 'SpreadElement') { + const [get] = init_spread_bindings(expression, context); expression = b.call(get); } else if (expression.type === 'SequenceExpression') { expression = b.call(expression.expressions[0]); @@ -134,7 +140,7 @@ export function build_element_attributes(node, context, transform) { } else if ( attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression' && - !attribute.metadata.spread_binding + 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/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 609447bcf0..f302603409 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -77,9 +77,6 @@ export class ExpressionMetadata { /** True if the expression contains `await` */ has_await = false; - /** True if the expression includes a spread element */ - has_spread = false; - /** True if the expression includes a member expression */ has_member_expression = false; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index e4e81a3cec..2e51a1d50a 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 { ChainExpression, SimpleCallExpression, SequenceExpression, - SourceLocation + SourceLocation, + SpreadElement } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; @@ -207,13 +208,12 @@ export namespace AST { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression | SequenceExpression; + expression: Identifier | MemberExpression | SequenceExpression | SpreadElement; /** @internal */ metadata: { binding?: Binding | null; binding_group_name: Identifier; parent_each_blocks: EachBlock[]; - spread_binding: boolean; expression: ExpressionMetadata; }; }