Skip to content

Commit 11e4ba0

Browse files
committed
feat: add support for bind getters/setters
1 parent ac9b7de commit 11e4ba0

File tree

10 files changed

+155
-78
lines changed

10 files changed

+155
-78
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export function BindDirective(node, context) {
2020
validate_no_const_assignment(node, node.expression, context.state.scope, true);
2121

2222
const assignee = node.expression;
23+
24+
if (assignee.type === 'SequenceExpression') {
25+
if (assignee.expressions.length !== 2) {
26+
e.bind_invalid_expression(node);
27+
}
28+
return;
29+
}
30+
2331
const left = object(assignee);
2432

2533
if (left === null) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,26 @@ export function BindDirective(node, context) {
3636
);
3737
}
3838

39-
const get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
40-
41-
/** @type {Expression | undefined} */
42-
let set = b.unthunk(
43-
b.arrow(
44-
[b.id('$$value')],
45-
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
46-
)
47-
);
48-
49-
if (get === set) {
50-
set = undefined;
39+
let get, set;
40+
41+
if (expression.type === 'SequenceExpression') {
42+
const [get_expression, set_expression] = expression.expressions;
43+
get = /** @type {Expression} */ (context.visit(get_expression));
44+
set = /** @type {Expression} */ (context.visit(set_expression));
45+
} else {
46+
get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
47+
48+
/** @type {Expression | undefined} */
49+
set = b.unthunk(
50+
b.arrow(
51+
[b.id('$$value')],
52+
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
53+
)
54+
);
55+
56+
if (get === set) {
57+
set = undefined;
58+
}
5159
}
5260

5361
/** @type {CallExpression} */

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) {
450450
if (context.state.analysis.runes) return;
451451

452452
let bound = value_binding.expression;
453+
454+
if (bound.type === 'SequenceExpression') {
455+
return;
456+
}
457+
453458
while (bound.type === 'MemberExpression') {
454459
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
455460
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -161,49 +161,63 @@ export function build_component(node, component_name, context, anchor = context.
161161
push_prop(b.init(attribute.name, value));
162162
}
163163
} else if (attribute.type === 'BindDirective') {
164-
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
165-
166-
if (
167-
dev &&
168-
expression.type === 'MemberExpression' &&
169-
context.state.analysis.runes &&
170-
!is_ignored(node, 'binding_property_non_reactive')
171-
) {
172-
validate_binding(context.state, attribute, expression);
173-
}
174-
175-
if (attribute.name === 'this') {
176-
bind_this = attribute.expression;
164+
if (attribute.expression.type === 'SequenceExpression') {
165+
const [get_expression, set_expression] = attribute.expression.expressions;
166+
const get = /** @type {Expression} */ (context.visit(get_expression));
167+
const set = /** @type {Expression} */ (context.visit(set_expression));
168+
const get_id = b.id(context.state.scope.generate('bind_get'));
169+
const set_id = b.id(context.state.scope.generate('bind_set'));
170+
171+
context.state.init.push(b.var(get_id, get));
172+
context.state.init.push(b.var(set_id, set));
173+
174+
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
175+
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
177176
} else {
178-
if (dev) {
179-
binding_initializers.push(
180-
b.stmt(
181-
b.call(
182-
b.id('$.add_owner_effect'),
183-
b.thunk(expression),
184-
b.id(component_name),
185-
is_ignored(node, 'ownership_invalid_binding') && b.true
186-
)
187-
)
188-
);
177+
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
178+
179+
if (
180+
dev &&
181+
expression.type === 'MemberExpression' &&
182+
context.state.analysis.runes &&
183+
!is_ignored(node, 'binding_property_non_reactive')
184+
) {
185+
validate_binding(context.state, attribute, expression);
189186
}
190187

191-
const is_store_sub =
192-
attribute.expression.type === 'Identifier' &&
193-
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
194-
195-
if (is_store_sub) {
188+
if (attribute.name === 'this') {
189+
bind_this = attribute.expression;
190+
} else {
191+
if (dev) {
192+
binding_initializers.push(
193+
b.stmt(
194+
b.call(
195+
b.id('$.add_owner_effect'),
196+
b.thunk(expression),
197+
b.id(component_name),
198+
is_ignored(node, 'ownership_invalid_binding') && b.true
199+
)
200+
)
201+
);
202+
}
203+
204+
const is_store_sub =
205+
attribute.expression.type === 'Identifier' &&
206+
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
207+
208+
if (is_store_sub) {
209+
push_prop(
210+
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
211+
);
212+
} else {
213+
push_prop(b.get(attribute.name, [b.return(expression)]));
214+
}
215+
216+
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
196217
push_prop(
197-
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
218+
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
198219
);
199-
} else {
200-
push_prop(b.get(attribute.name, [b.return(expression)]));
201220
}
202-
203-
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
204-
push_prop(
205-
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
206-
);
207221
}
208222
}
209223
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
1+
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
22
/** @import { AST, SvelteNode } from '#compiler' */
33
/** @import { ComponentClientTransformState } from '../../types' */
44
import { walk } from 'zimmerframe';
@@ -157,11 +157,19 @@ export function build_update_assignment(state, id, init, value, update) {
157157

158158
/**
159159
* Serializes `bind:this` for components and elements.
160-
* @param {Identifier | MemberExpression} expression
160+
* @param {Identifier | MemberExpression | SequenceExpression} expression
161161
* @param {Expression} value
162162
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
163163
*/
164164
export function build_bind_this(expression, value, { state, visit }) {
165+
if (expression.type === 'SequenceExpression') {
166+
const [get_expression, set_expression] = expression.expressions;
167+
const get = /** @type {Expression} */ (visit(get_expression));
168+
const set = /** @type {Expression} */ (visit(set_expression));
169+
170+
return b.call('$.bind_this', value, get, set);
171+
}
172+
165173
/** @type {Identifier[]} */
166174
const ids = [];
167175

@@ -238,6 +246,9 @@ export function build_bind_this(expression, value, { state, visit }) {
238246
* @param {MemberExpression} expression
239247
*/
240248
export function validate_binding(state, binding, expression) {
249+
if (binding.expression.type === 'SequenceExpression') {
250+
return;
251+
}
241252
// If we are referencing a $store.foo then we don't need to add validation
242253
const left = object(binding.expression);
243254
const left_binding = left && state.scope.get(left.name);

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,36 @@ export function build_inline_component(node, expression, context) {
8181
const value = build_attribute_value(attribute.value, context, false, true);
8282
push_prop(b.prop('init', b.key(attribute.name), value));
8383
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
84-
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
85-
push_prop(
86-
b.get(attribute.name, [
87-
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
88-
])
89-
);
90-
push_prop(
91-
b.set(attribute.name, [
92-
b.stmt(
93-
/** @type {Expression} */ (
94-
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
95-
)
96-
),
97-
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
98-
])
99-
);
84+
if (attribute.expression.type === 'SequenceExpression') {
85+
const [get_expression, set_expression] = attribute.expression.expressions;
86+
const get = /** @type {Expression} */ (context.visit(get_expression));
87+
const set = /** @type {Expression} */ (context.visit(set_expression));
88+
const get_id = b.id(context.state.scope.generate('bind_get'));
89+
const set_id = b.id(context.state.scope.generate('bind_set'));
90+
91+
context.state.init.push(b.var(get_id, get));
92+
context.state.init.push(b.var(set_id, set));
93+
94+
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
95+
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
96+
} else {
97+
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
98+
push_prop(
99+
b.get(attribute.name, [
100+
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
101+
])
102+
);
103+
push_prop(
104+
b.set(attribute.name, [
105+
b.stmt(
106+
/** @type {Expression} */ (
107+
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
108+
)
109+
),
110+
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
111+
])
112+
);
113+
}
100114
}
101115
}
102116

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,16 @@ export function build_element_attributes(node, context) {
110110
if (binding?.omit_in_ssr) continue;
111111

112112
if (is_content_editable_binding(attribute.name)) {
113-
content = /** @type {Expression} */ (context.visit(attribute.expression));
113+
content =
114+
attribute.expression.type === 'SequenceExpression'
115+
? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0])))
116+
: /** @type {Expression} */ (context.visit(attribute.expression));
114117
} else if (attribute.name === 'value' && node.name === 'textarea') {
115118
content = b.call(
116119
'$.escape',
117-
/** @type {Expression} */ (context.visit(attribute.expression))
120+
attribute.expression.type === 'SequenceExpression'
121+
? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0])))
122+
: /** @type {Expression} */ (context.visit(attribute.expression))
118123
);
119124
} else if (attribute.name === 'group') {
120125
const value_attribute = /** @type {AST.Attribute | undefined} */ (
@@ -129,6 +134,11 @@ export function build_element_attributes(node, context) {
129134
is_text_attribute(attr) &&
130135
attr.value[0].data === 'checkbox'
131136
);
137+
const attribute_expression =
138+
attribute.expression.type === 'SequenceExpression'
139+
? b.call(attribute.expression.expressions[0])
140+
: attribute.expression;
141+
132142
attributes.push(
133143
create_attribute('checked', -1, -1, [
134144
{
@@ -138,12 +148,12 @@ export function build_element_attributes(node, context) {
138148
parent: attribute,
139149
expression: is_checkbox
140150
? b.call(
141-
b.member(attribute.expression, 'includes'),
151+
b.member(attribute_expression, 'includes'),
142152
build_attribute_value(value_attribute.value, context)
143153
)
144154
: b.binary(
145155
'===',
146-
attribute.expression,
156+
attribute_expression,
147157
build_attribute_value(value_attribute.value, context)
148158
),
149159
metadata: {
@@ -153,14 +163,19 @@ export function build_element_attributes(node, context) {
153163
])
154164
);
155165
} else {
166+
const attribute_expression =
167+
attribute.expression.type === 'SequenceExpression'
168+
? b.call(attribute.expression.expressions[0])
169+
: attribute.expression;
170+
156171
attributes.push(
157172
create_attribute(attribute.name, -1, -1, [
158173
{
159174
type: 'ExpressionTag',
160175
start: -1,
161176
end: -1,
162177
parent: attribute,
163-
expression: attribute.expression,
178+
expression: attribute_expression,
164179
metadata: {
165180
expression: create_expression_metadata()
166181
}

packages/svelte/src/compiler/types/legacy-nodes.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type {
66
Identifier,
77
MemberExpression,
88
ObjectExpression,
9-
Pattern
9+
Pattern,
10+
SequenceExpression
1011
} from 'estree';
1112

1213
interface BaseNode {
@@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode {
4950
/** The 'x' in `bind:x` */
5051
name: string;
5152
/** The y in `bind:x={y}` */
52-
expression: Identifier | MemberExpression;
53+
expression: Identifier | MemberExpression | SequenceExpression;
5354
}
5455

5556
export interface LegacyBody extends BaseElement {

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import type {
1414
Pattern,
1515
Program,
1616
ChainExpression,
17-
SimpleCallExpression
17+
SimpleCallExpression,
18+
SequenceExpression
1819
} from 'estree';
1920
import type { Scope } from '../phases/scope';
2021

@@ -185,7 +186,7 @@ export namespace AST {
185186
/** The 'x' in `bind:x` */
186187
name: string;
187188
/** The y in `bind:x={y}` */
188-
expression: Identifier | MemberExpression;
189+
expression: Identifier | MemberExpression | SequenceExpression;
189190
/** @internal */
190191
metadata: {
191192
binding_group_name: Identifier;

packages/svelte/types/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ declare module 'svelte/animate' {
606606
}
607607

608608
declare module 'svelte/compiler' {
609-
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression } from 'estree';
609+
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
610610
import type { SourceMap } from 'magic-string';
611611
import type { Location } from 'locate-character';
612612
/**
@@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' {
10471047
/** The 'x' in `bind:x` */
10481048
name: string;
10491049
/** The y in `bind:x={y}` */
1050-
expression: Identifier | MemberExpression;
1050+
expression: Identifier | MemberExpression | SequenceExpression;
10511051
}
10521052

10531053
/** A `class:` directive */

0 commit comments

Comments
 (0)