diff --git a/.changeset/yellow-dodos-smell.md b/.changeset/yellow-dodos-smell.md new file mode 100644 index 000000000000..ea2aead6622e --- /dev/null +++ b/.changeset/yellow-dodos-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly add owners to function bindings diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 068971145c6e..e3492bfebe57 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -2,7 +2,11 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; -import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; +import { + extract_all_identifiers_from_expression, + get_attribute_chunks, + object +} from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { create_derived } from '../../utils.js'; import { build_bind_this, validate_binding } from '../shared/utils.js'; @@ -93,6 +97,8 @@ export function build_component(node, component_name, context, anchor = context. } } + const ownerships_effects = new Map(); + for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { @@ -185,17 +191,23 @@ export function build_component(node, component_name, context, anchor = context. // Only run ownership addition on $state fields. // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. - if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') { - binding_initializers.push( - b.stmt( - b.call( - b.id('$.add_owner_effect'), - b.thunk(expression), - b.id(component_name), - is_ignored(node, 'ownership_invalid_binding') && b.true + if ( + binding?.kind !== 'derived' && + binding?.kind !== 'raw_state' && + !ownerships_effects.has(left?.name) + ) { + ownerships_effects.set(left?.name, () => { + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + b.thunk(expression), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) ) - ) - ); + ); + }); } } @@ -212,6 +224,32 @@ export function build_component(node, component_name, context, anchor = context. push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + if (dev) { + const [, get_ids] = extract_all_identifiers_from_expression(get); + + for (let get_id of get_ids) { + const binding = context.state.scope.get(get_id.name); + if ( + binding && + binding.kind !== 'derived' && + binding.kind !== 'raw_state' && + !ownerships_effects.has(get_id.name) + ) { + ownerships_effects.set(get_id.name, () => { + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + b.thunk(get_id), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) + ) + ); + }); + } + } + } } } else { if ( @@ -255,6 +293,10 @@ export function build_component(node, component_name, context, anchor = context. } } + for (let [, ownership_effect] of ownerships_effects) { + ownership_effect(); + } + delayed_props.forEach((fn) => fn()); if (slot_scope_applies_to_itself) { diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte new file mode 100644 index 000000000000..ef91b0756dfb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/Child.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js new file mode 100644 index 000000000000..4c77aea20684 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + test({ target, warnings, assert }) { + const btn = target.querySelector('button'); + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(warnings, []); + + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte new file mode 100644 index 000000000000..4a3ce82726b6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-function-bindings/main.svelte @@ -0,0 +1,10 @@ + + + len % 2 === 0 ? arr : arr2, (v) => {}} /> \ No newline at end of file