fix: ensure bindings always take precedence over spreads (#14575)

pull/14576/head
Simon H 3 weeks ago committed by GitHub
parent 6a6b4ec36a
commit ca67aa1b34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure bindings always take precedence over spreads

@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js';
export function build_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {ExpressionStatement[]} */
const lets = [];
@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context.
/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}
@ -202,22 +213,27 @@ export function build_component(node, component_name, context, anchor = context.
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)])
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)]));
push_prop(b.get(attribute.name, [b.return(expression)]), true);
}
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
true
);
}
}
}
delayed_props.forEach((fn) => fn());
if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}

@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js';
export function build_inline_component(node, expression, context) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {Property[]} */
const custom_css_props = [];
@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) {
/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}
@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} 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
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
]),
true
);
push_prop(
b.set(attribute.name, [
@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) {
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
]),
true
);
}
}
delayed_props.forEach((fn) => fn());
/** @type {Statement[]} */
const snippet_declarations = [];

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
ssrHtml: `<input value="foo">`,
test({ assert, target }) {
assert.equal(target.querySelector('input')?.value, 'foo');
}
});

@ -0,0 +1,5 @@
<script>
let { value = $bindable(), ...properties } = $props();
</script>
<input bind:value {...properties} />

@ -0,0 +1,11 @@
<script>
import Button from './input.svelte';
let value = $state('foo');
const props = $state({
value: 'bar'
});
</script>
<Button bind:value {...props} />
Loading…
Cancel
Save