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 da2502b6f5..90b2a36f95 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 884a7addfa..4743edef5e 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 (!state.analysis.runes || binding.reassigned || binding.initial || binding.mutated) { return b.call(node); } @@ -391,7 +391,7 @@ 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 || (binding.mutated && binding.kind === 'bindable_prop')) { if (binding.kind === 'bindable_prop') { return b.call( left, @@ -402,7 +402,8 @@ export function serialize_set_binding(node, context, fallback, prefix, options) value ), b.call(left) - ]) + ]), + b.true ); } else { return b.call( @@ -539,6 +540,7 @@ function get_hoistable_params(node, context) { // 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.mutated && // this can be removed once legacy mode is gone binding.initial === null && !context.state.analysis.accessors ) { @@ -602,7 +604,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; } 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 b35e01b2d7..94347a7111 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 @@ -240,7 +240,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 +268,9 @@ export const javascript_visitors_runes = { initial = b.call('$.proxy', initial); } - if (binding.reassigned || state.analysis.accessors || 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 + if (binding.reassigned || binding.mutated || state.analysis.accessors || initial) { 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 e7d01f33db..3134409b85 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 5d4634f913..f57a9f448d 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 {V} */ 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)(value); + } return value; } else { return getter(); 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 0000000000..9a21ff4a1a --- /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 0000000000..9f9724c9a4 --- /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 0000000000..765355fbf3 --- /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 0000000000..26484ff9ef --- /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 0000000000..9e204c224e --- /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 0000000000..31ed1a0c97 --- /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 9dd2ef3048..c766ee0a79 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