From cc273f7d53e6247b7197c683336ef45df1b0ce32 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 14 Feb 2024 16:08:27 +0000 Subject: [PATCH] fix: prevent infinite loop when writing to store using shorthand (#10477) Fixes #10472 This PR ensures we untrack parts of the compiled output to a store write, such as that this no longer brings up an infinite updates error --- .changeset/old-jokes-deliver.md | 5 +++ .../phases/3-transform/client/utils.js | 33 +++++++++++++++++-- .../samples/state-store/_config.js | 26 +++++++++++++++ .../samples/state-store/main.svelte | 16 +++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 .changeset/old-jokes-deliver.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-store/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-store/main.svelte diff --git a/.changeset/old-jokes-deliver.md b/.changeset/old-jokes-deliver.md new file mode 100644 index 0000000000..1aa08fe7be --- /dev/null +++ b/.changeset/old-jokes-deliver.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: prevent infinite loop when writing to store using shorthand 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 ee2ed36544..903106e7e1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -418,15 +418,44 @@ export function serialize_set_binding(node, context, fallback, options) { } } else { if (is_store) { + // If we are assigning to a store property, we need to ensure we don't + // capture the read for the store as part of the member expression to + // keep consistency with how store $ shorthand reads work in Svelte 4. + /** + * + * @param {import("estree").Expression | import("estree").Pattern} node + * @returns {import("estree").Expression} + */ + function visit_node(node) { + if (node.type === 'MemberExpression') { + return { + ...node, + object: visit_node(/** @type {import("estree").Expression} */ (node.object)), + property: /** @type {import("estree").Expression} */ (visit(node.property)) + }; + } + if (node.type === 'Identifier') { + const binding = state.scope.get(node.name); + + if (binding !== null && binding.kind === 'store_sub') { + return b.call( + '$.untrack', + b.thunk(/** @type {import('estree').Expression} */ (visit(node))) + ); + } + } + return /** @type {import("estree").Expression} */ (visit(node)); + } + return b.call( '$.mutate_store', serialize_get_binding(b.id(left_name), state), b.assignment( node.operator, - /** @type {import('estree').Pattern} */ (visit(node.left)), + /** @type {import("estree").Pattern}} */ (visit_node(node.left)), value ), - b.call('$' + left_name) + b.call('$.untrack', b.id('$' + left_name)) ); } else if (!state.analysis.runes) { if (binding.kind === 'prop') { diff --git a/packages/svelte/tests/runtime-runes/samples/state-store/_config.js b/packages/svelte/tests/runtime-runes/samples/state-store/_config.js new file mode 100644 index 0000000000..cfdcc0433d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-store/_config.js @@ -0,0 +1,26 @@ +import { flushSync } from '../../../../src/main/main-client'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

test_store:\n 4

counter:\n 4

` + ); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

test_store:\n 5

counter:\n 5

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-store/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-store/main.svelte new file mode 100644 index 0000000000..5b2c31c915 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-store/main.svelte @@ -0,0 +1,16 @@ + + + +

test_store: {$test_store.id}

+

counter: {counter}

+ +