diff --git a/.changeset/nervous-ducks-repeat.md b/.changeset/nervous-ducks-repeat.md new file mode 100644 index 000000000000..3d07becb0917 --- /dev/null +++ b/.changeset/nervous-ducks-repeat.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: better binding interop between runes/non-runes components diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index da2502b6f512..90b2a36f9511 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -363,7 +363,12 @@ export function client_component(source, analysis, options) { } if (analysis.uses_props || analysis.uses_rest_props) { - const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')]; + const to_remove = [ + b.literal('children'), + b.literal('$$slots'), + b.literal('$$events'), + b.literal('$$legacy') + ]; if (analysis.custom_element) { to_remove.push(b.literal('$$host')); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 884a7addfadd..c716bb83b694 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -89,7 +89,7 @@ export function serialize_get_binding(node, state) { } if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { - if (!state.analysis.runes || binding.reassigned || binding.initial) { + if (is_prop_source(binding, state)) { return b.call(node); } @@ -391,18 +391,20 @@ export function serialize_set_binding(node, context, fallback, prefix, options) ), b.call('$.untrack', b.id('$' + left_name)) ); - } else if (!state.analysis.runes) { + } else if ( + !state.analysis.runes || + // this condition can go away once legacy mode is gone; only necessary for interop with legacy parent bindings + (binding.mutated && binding.kind === 'bindable_prop') + ) { if (binding.kind === 'bindable_prop') { return b.call( left, - b.sequence([ - b.assignment( - node.operator, - /** @type {import('estree').Pattern} */ (visit(node.left)), - value - ), - b.call(left) - ]) + b.assignment( + node.operator, + /** @type {import('estree').Pattern} */ (visit(node.left)), + value + ), + b.true ); } else { return b.call( @@ -538,9 +540,7 @@ function get_hoistable_params(node, context) { } else if ( // If we are referencing a simple $$props value, then we need to reference the object property instead (binding.kind === 'prop' || binding.kind === 'bindable_prop') && - !binding.reassigned && - binding.initial === null && - !context.state.analysis.accessors + !is_prop_source(binding, context.state) ) { push_unique(b.id('$$props')); } else { @@ -602,7 +602,9 @@ export function get_prop_source(binding, state, name, initial) { if ( state.analysis.accessors || - (state.analysis.immutable ? binding.reassigned : binding.mutated) + (state.analysis.immutable + ? binding.reassigned || (state.analysis.runes && binding.mutated) + : binding.mutated) ) { flags |= PROPS_IS_UPDATED; } @@ -637,6 +639,25 @@ export function get_prop_source(binding, state, name, initial) { return b.call('$.prop', ...args); } +/** + * + * @param {import('#compiler').Binding} binding + * @param {import('./types').ClientTransformState} state + * @returns + */ +export function is_prop_source(binding, state) { + return ( + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && + (!state.analysis.runes || + state.analysis.accessors || + binding.reassigned || + binding.initial || + // Until legacy mode is gone, we also need to use the prop source when only mutated is true, + // because the parent could be a legacy component which needs coarse-grained reactivity + binding.mutated) + ); +} + /** * @param {import('estree').Expression} node * @param {import("../../scope.js").Scope | null} scope diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index b35e01b2d7e7..91934738b0db 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -4,6 +4,7 @@ import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; import { get_prop_source, + is_prop_source, is_state_source, serialize_proxy_reassignment, should_proxy_or_freeze @@ -240,7 +241,11 @@ export const javascript_visitors_runes = { assert.equal(declarator.id.type, 'ObjectPattern'); /** @type {string[]} */ - const seen = []; + const seen = ['$$slots', '$$events', '$$legacy']; + + if (state.analysis.custom_element) { + seen.push('$$host'); + } for (const property of declarator.id.properties) { if (property.type === 'Property') { @@ -264,7 +269,7 @@ export const javascript_visitors_runes = { initial = b.call('$.proxy', initial); } - if (binding.reassigned || state.analysis.accessors || initial) { + if (is_prop_source(binding, state)) { declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); } } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index e7d01f33db94..3134409b85cc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -900,6 +900,10 @@ function serialize_inline_component(node, component_name, context) { push_prop(b.init('$$slots', b.object(serialized_slots))); } + if (!context.state.analysis.runes) { + push_prop(b.init('$$legacy', b.true)); + } + const props_expression = props_and_spreads.length === 0 || (props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0])) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5d4634f91394..3232239a75dd 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -267,9 +267,16 @@ export function prop(props, key, flags, fallback) { // intermediate mode — prop is written to, but the parent component had // `bind:foo` which means we can just call `$$props.foo = value` directly if (setter) { - return function (/** @type {V} */ value) { - if (arguments.length === 1) { - /** @type {Function} */ (setter)(value); + var legacy_parent = props.$$legacy; + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { + if (arguments.length > 0) { + // We don't want to notify if the value was mutated and the parent is in runes mode. + // In that case the state proxy (if it exists) should take care of the notification. + // If the parent is not in runes mode, we need to notify on mutation, too, that the prop + // has changed because the parent will not be able to detect the change otherwise. + if (!runes || !mutation || legacy_parent) { + /** @type {Function} */ (setter)(mutation ? getter() : value); + } return value; } else { return getter(); @@ -302,7 +309,7 @@ export function prop(props, key, flags, fallback) { if (!immutable) current_value.equals = safe_equals; - return function (/** @type {V} */ value) { + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { var current = get(current_value); // legacy nonsense — need to ensure the source is invalidated when necessary @@ -318,9 +325,11 @@ export function prop(props, key, flags, fallback) { } if (arguments.length > 0) { - if (!current_value.equals(value)) { + const new_value = mutation ? get(current_value) : value; + + if (!current_value.equals(new_value)) { from_child = true; - set(inner_current_value, value); + set(inner_current_value, new_value); get(current_value); // force a synchronisation immediately } diff --git a/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/_config.js b/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/_config.js new file mode 100644 index 000000000000..c07346d55d0a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + test({ assert, logs }) { + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/main.svelte b/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/main.svelte new file mode 100644 index 000000000000..2ad07e1253c9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/mutation-correct-return-value/main.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/Component1.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop/Component1.svelte new file mode 100644 index 000000000000..9a21ff4a1a32 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/Component1.svelte @@ -0,0 +1,9 @@ + + +{#if primitive} + +{:else} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/Component2.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop/Component2.svelte new file mode 100644 index 000000000000..9f9724c9a4c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/Component2.svelte @@ -0,0 +1,9 @@ + + +{#if primitive} + +{:else} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/Legacy.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop/Legacy.svelte new file mode 100644 index 000000000000..765355fbf333 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/Legacy.svelte @@ -0,0 +1,24 @@ + + + + +{object1.value} + + +{object2.value} + + +{primitive1} + + +{primitive2} + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/Runes.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop/Runes.svelte new file mode 100644 index 000000000000..26484ff9effd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/Runes.svelte @@ -0,0 +1,39 @@ + + +{object1.value} + + +{object2.value} + + + +{#if true} + {object3.value} + + + {object4.value} + +{/if} + +{primitive1} + + +{primitive2} + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-interop/_config.js new file mode 100644 index 000000000000..9e204c224e19 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/_config.js @@ -0,0 +1,24 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const buttons = target.querySelectorAll('button'); + + for (const button of buttons) { + await button.click(); + flushSync(); + } + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + bar bar bar bar +
+ bar bar foo foo bar bar + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop/main.svelte new file mode 100644 index 000000000000..31ed1a0c9783 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop/main.svelte @@ -0,0 +1,10 @@ + + + + +
+ + diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index 9dd2ef3048ce..c766ee0a79b4 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -5,6 +5,6 @@ export default function Bind_this($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo); + $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); $.append($$anchor, fragment); } \ No newline at end of file