feat: add support for bind getters/setters

bind-get-set
Dominic Gannaway 1 day ago
parent ac9b7de058
commit 11e4ba017e

@ -20,6 +20,14 @@ export function BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true); validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression; const assignee = node.expression;
if (assignee.type === 'SequenceExpression') {
if (assignee.expressions.length !== 2) {
e.bind_invalid_expression(node);
}
return;
}
const left = object(assignee); const left = object(assignee);
if (left === null) { if (left === null) {

@ -36,18 +36,26 @@ export function BindDirective(node, context) {
); );
} }
const get = b.thunk(/** @type {Expression} */ (context.visit(expression))); let get, set;
/** @type {Expression | undefined} */ if (expression.type === 'SequenceExpression') {
let set = b.unthunk( const [get_expression, set_expression] = expression.expressions;
b.arrow( get = /** @type {Expression} */ (context.visit(get_expression));
[b.id('$$value')], set = /** @type {Expression} */ (context.visit(set_expression));
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) } else {
) get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
);
/** @type {Expression | undefined} */
if (get === set) { set = b.unthunk(
set = undefined; b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
)
);
if (get === set) {
set = undefined;
}
} }
/** @type {CallExpression} */ /** @type {CallExpression} */

@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return; if (context.state.analysis.runes) return;
let bound = value_binding.expression; let bound = value_binding.expression;
if (bound.type === 'SequenceExpression') {
return;
}
while (bound.type === 'MemberExpression') { while (bound.type === 'MemberExpression') {
bound = /** @type {Identifier | MemberExpression} */ (bound.object); bound = /** @type {Identifier | MemberExpression} */ (bound.object);
} }

@ -161,49 +161,63 @@ export function build_component(node, component_name, context, anchor = context.
push_prop(b.init(attribute.name, value)); push_prop(b.init(attribute.name, value));
} }
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression)); if (attribute.expression.type === 'SequenceExpression') {
const [get_expression, set_expression] = attribute.expression.expressions;
if ( const get = /** @type {Expression} */ (context.visit(get_expression));
dev && const set = /** @type {Expression} */ (context.visit(set_expression));
expression.type === 'MemberExpression' && const get_id = b.id(context.state.scope.generate('bind_get'));
context.state.analysis.runes && const set_id = b.id(context.state.scope.generate('bind_set'));
!is_ignored(node, 'binding_property_non_reactive')
) { context.state.init.push(b.var(get_id, get));
validate_binding(context.state, attribute, expression); context.state.init.push(b.var(set_id, set));
}
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
if (attribute.name === 'this') { push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
bind_this = attribute.expression;
} else { } else {
if (dev) { const expression = /** @type {Expression} */ (context.visit(attribute.expression));
binding_initializers.push(
b.stmt( if (
b.call( dev &&
b.id('$.add_owner_effect'), expression.type === 'MemberExpression' &&
b.thunk(expression), context.state.analysis.runes &&
b.id(component_name), !is_ignored(node, 'binding_property_non_reactive')
is_ignored(node, 'ownership_invalid_binding') && b.true ) {
) validate_binding(context.state, attribute, expression);
)
);
} }
const is_store_sub = if (attribute.name === 'this') {
attribute.expression.type === 'Identifier' && bind_this = attribute.expression;
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; } else {
if (dev) {
if (is_store_sub) { 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( 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)))])
);
} }
} }
} }

@ -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 { AST, SvelteNode } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */ /** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe'; 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. * Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} expression * @param {Identifier | MemberExpression | SequenceExpression} expression
* @param {Expression} value * @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context * @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
*/ */
export function build_bind_this(expression, value, { state, visit }) { 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[]} */ /** @type {Identifier[]} */
const ids = []; const ids = [];
@ -238,6 +246,9 @@ export function build_bind_this(expression, value, { state, visit }) {
* @param {MemberExpression} expression * @param {MemberExpression} expression
*/ */
export function validate_binding(state, binding, 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 // If we are referencing a $store.foo then we don't need to add validation
const left = object(binding.expression); const left = object(binding.expression);
const left_binding = left && state.scope.get(left.name); const left_binding = left && state.scope.get(left.name);

@ -81,22 +81,36 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true); const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } 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 if (attribute.expression.type === 'SequenceExpression') {
push_prop( const [get_expression, set_expression] = attribute.expression.expressions;
b.get(attribute.name, [ const get = /** @type {Expression} */ (context.visit(get_expression));
b.return(/** @type {Expression} */ (context.visit(attribute.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'));
push_prop(
b.set(attribute.name, [ context.state.init.push(b.var(get_id, get));
b.stmt( context.state.init.push(b.var(set_id, set));
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) 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 {
b.stmt(b.assignment('=', b.id('$$settled'), b.false)) // 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))
])
);
}
} }
} }

@ -110,11 +110,16 @@ export function build_element_attributes(node, context) {
if (binding?.omit_in_ssr) continue; if (binding?.omit_in_ssr) continue;
if (is_content_editable_binding(attribute.name)) { 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') { } else if (attribute.name === 'value' && node.name === 'textarea') {
content = b.call( content = b.call(
'$.escape', '$.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') { } else if (attribute.name === 'group') {
const value_attribute = /** @type {AST.Attribute | undefined} */ ( const value_attribute = /** @type {AST.Attribute | undefined} */ (
@ -129,6 +134,11 @@ export function build_element_attributes(node, context) {
is_text_attribute(attr) && is_text_attribute(attr) &&
attr.value[0].data === 'checkbox' attr.value[0].data === 'checkbox'
); );
const attribute_expression =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: attribute.expression;
attributes.push( attributes.push(
create_attribute('checked', -1, -1, [ create_attribute('checked', -1, -1, [
{ {
@ -138,12 +148,12 @@ export function build_element_attributes(node, context) {
parent: attribute, parent: attribute,
expression: is_checkbox expression: is_checkbox
? b.call( ? b.call(
b.member(attribute.expression, 'includes'), b.member(attribute_expression, 'includes'),
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context)
) )
: b.binary( : b.binary(
'===', '===',
attribute.expression, attribute_expression,
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context)
), ),
metadata: { metadata: {
@ -153,6 +163,11 @@ export function build_element_attributes(node, context) {
]) ])
); );
} else { } else {
const attribute_expression =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: attribute.expression;
attributes.push( attributes.push(
create_attribute(attribute.name, -1, -1, [ create_attribute(attribute.name, -1, -1, [
{ {
@ -160,7 +175,7 @@ export function build_element_attributes(node, context) {
start: -1, start: -1,
end: -1, end: -1,
parent: attribute, parent: attribute,
expression: attribute.expression, expression: attribute_expression,
metadata: { metadata: {
expression: create_expression_metadata() expression: create_expression_metadata()
} }

@ -6,7 +6,8 @@ import type {
Identifier, Identifier,
MemberExpression, MemberExpression,
ObjectExpression, ObjectExpression,
Pattern Pattern,
SequenceExpression
} from 'estree'; } from 'estree';
interface BaseNode { interface BaseNode {
@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression; expression: Identifier | MemberExpression | SequenceExpression;
} }
export interface LegacyBody extends BaseElement { export interface LegacyBody extends BaseElement {

@ -14,7 +14,8 @@ import type {
Pattern, Pattern,
Program, Program,
ChainExpression, ChainExpression,
SimpleCallExpression SimpleCallExpression,
SequenceExpression
} from 'estree'; } from 'estree';
import type { Scope } from '../phases/scope'; import type { Scope } from '../phases/scope';
@ -185,7 +186,7 @@ export namespace AST {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression; expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */ /** @internal */
metadata: { metadata: {
binding_group_name: Identifier; binding_group_name: Identifier;

@ -606,7 +606,7 @@ declare module 'svelte/animate' {
} }
declare module 'svelte/compiler' { 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 { SourceMap } from 'magic-string';
import type { Location } from 'locate-character'; import type { Location } from 'locate-character';
/** /**
@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' {
/** The 'x' in `bind:x` */ /** The 'x' in `bind:x` */
name: string; name: string;
/** The y in `bind:x={y}` */ /** The y in `bind:x={y}` */
expression: Identifier | MemberExpression; expression: Identifier | MemberExpression | SequenceExpression;
} }
/** A `class:` directive */ /** A `class:` directive */

Loading…
Cancel
Save