Skip to content

Commit 84ff1d4

Browse files
committed
fix: ensure bindings always take precedence over spreads
1 parent 0a9890b commit 84ff1d4

File tree

6 files changed

+81
-20
lines changed

6 files changed

+81
-20
lines changed

.changeset/little-berries-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure bindings always take precedence over spreads

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js';
2020
export function build_component(node, component_name, context, anchor = context.state.node) {
2121
/** @type {Array<Property[] | Expression>} */
2222
const props_and_spreads = [];
23+
/** @type {Array<() => void>} */
24+
const delayed_props = [];
2325

2426
/** @type {ExpressionStatement[]} */
2527
const lets = [];
@@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context.
6365

6466
/**
6567
* @param {Property} prop
68+
* @param {boolean} [delay]
6669
*/
67-
function push_prop(prop) {
68-
const current = props_and_spreads.at(-1);
69-
const current_is_props = Array.isArray(current);
70-
const props = current_is_props ? current : [];
71-
props.push(prop);
72-
if (!current_is_props) {
73-
props_and_spreads.push(props);
70+
function push_prop(prop, delay = false) {
71+
const do_push = () => {
72+
const current = props_and_spreads.at(-1);
73+
const current_is_props = Array.isArray(current);
74+
const props = current_is_props ? current : [];
75+
props.push(prop);
76+
if (!current_is_props) {
77+
props_and_spreads.push(props);
78+
}
79+
};
80+
81+
if (delay) {
82+
delayed_props.push(do_push);
83+
} else {
84+
do_push();
7485
}
7586
}
7687

@@ -202,22 +213,27 @@ export function build_component(node, component_name, context, anchor = context.
202213
attribute.expression.type === 'Identifier' &&
203214
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
204215

216+
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
205217
if (is_store_sub) {
206218
push_prop(
207-
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
219+
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
220+
true
208221
);
209222
} else {
210-
push_prop(b.get(attribute.name, [b.return(expression)]));
223+
push_prop(b.get(attribute.name, [b.return(expression)]), true);
211224
}
212225

213226
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
214227
push_prop(
215-
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
228+
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
229+
true
216230
);
217231
}
218232
}
219233
}
220234

235+
delayed_props.forEach((fn) => fn());
236+
221237
if (slot_scope_applies_to_itself) {
222238
context.state.init.push(...lets);
223239
}

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js';
1313
export function build_inline_component(node, expression, context) {
1414
/** @type {Array<Property[] | Expression>} */
1515
const props_and_spreads = [];
16+
/** @type {Array<() => void>} */
17+
const delayed_props = [];
1618

1719
/** @type {Property[]} */
1820
const custom_css_props = [];
@@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) {
4951

5052
/**
5153
* @param {Property} prop
54+
* @param {boolean} [delay]
5255
*/
53-
function push_prop(prop) {
54-
const current = props_and_spreads.at(-1);
55-
const current_is_props = Array.isArray(current);
56-
const props = current_is_props ? current : [];
57-
props.push(prop);
58-
if (!current_is_props) {
59-
props_and_spreads.push(props);
56+
function push_prop(prop, delay = false) {
57+
const do_push = () => {
58+
const current = props_and_spreads.at(-1);
59+
const current_is_props = Array.isArray(current);
60+
const props = current_is_props ? current : [];
61+
props.push(prop);
62+
if (!current_is_props) {
63+
props_and_spreads.push(props);
64+
}
65+
};
66+
67+
if (delay) {
68+
delayed_props.push(do_push);
69+
} else {
70+
do_push();
6071
}
6172
}
6273

@@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) {
8192
const value = build_attribute_value(attribute.value, context, false, true);
8293
push_prop(b.prop('init', b.key(attribute.name), value));
8394
} 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
95+
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
8596
push_prop(
8697
b.get(attribute.name, [
8798
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
88-
])
99+
]),
100+
true
89101
);
90102
push_prop(
91103
b.set(attribute.name, [
@@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) {
95107
)
96108
),
97109
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
98-
])
110+
]),
111+
true
99112
);
100113
}
101114
}
102115

116+
delayed_props.forEach((fn) => fn());
117+
103118
/** @type {Statement[]} */
104119
const snippet_declarations = [];
105120

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
ssrHtml: `<input value="foo">`,
5+
6+
test({ assert, target }) {
7+
assert.equal(target.querySelector('input')?.value, 'foo');
8+
}
9+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { value = $bindable(), ...properties } = $props();
3+
</script>
4+
5+
<input bind:value {...properties} />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import Button from './input.svelte';
3+
4+
let value = $state('foo');
5+
6+
const props = $state({
7+
value: 'bar'
8+
});
9+
</script>
10+
11+
<Button bind:value {...props} />

0 commit comments

Comments
 (0)