From d3fe49548ab832045ab208399c42dd59c8ebe7a8 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 00:09:39 +0100 Subject: [PATCH 1/4] Improve compiler output on legacy components to prevent infinite loops from bind:value on $: (block) with derived variable --- .../client/visitors/RegularElement.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index c6afdf67d314..1804e88a634e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -414,13 +414,20 @@ function setup_select_synchronization(value_binding, context) { bound = /** @type {Identifier | MemberExpression} */ (bound.object); } - // guard against reactively-derived bindings to prevent circular dependencies + // Skip synchronisation if the bound identifier is *already* updated by a + // reactive statement (declared directly in `$:` or assigned inside one). + // In those cases the extra invalidate-helper would re-write its own + // source signal and create a circular update loop. if (bound.type === 'Identifier') { const binding = context.state.scope.get(bound.name); - if (binding && binding.kind === 'legacy_reactive') { - // skip synchronization for reactive-derived bindings, - // the reactive statement already handles updates properly - return; + if (binding) { + // 1) declared directly inside a `$:` + if (binding.kind === 'legacy_reactive') return; + + // 2) declared elsewhere but *assigned* inside a `$:` block + for (const [, rs] of context.state.analysis.reactive_statements) { + if (rs.assignments.has(binding)) return; + } } } From 27b37bb143594c74087d967bedce2da305ab7e73 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 00:10:11 +0100 Subject: [PATCH 3/4] Add tests --- .../binding-select-reactive-block/_config.js | 46 +++++++++++++++++++ .../binding-select-reactive-block/main.svelte | 41 +++++++++++++++++ .../_config.js | 46 +++++++++++++++++++ .../main.svelte | 38 +++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/main.svelte diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/_config.js new file mode 100644 index 000000000000..79d8ffff067a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/_config.js @@ -0,0 +1,46 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + html: ` + + + + + `, + + async test({ assert, component, window, logs }) { + // Primary assertion: No infinite loop error + assert.notInclude(logs, 'Infinite loop detected'); + + // Verify component state + const select = window.document.querySelector('select'); + if (!select) { + assert.fail('Select element not found'); + return; + } + + // With default_details fallback nothing is selected + assert.equal(select.value, ''); + assert.equal(select.disabled, false); + + window.document.getElementById('btn-us')?.click(); + await tick(); + assert.equal(select.disabled, true); + assert.equal(select.value, 'US'); + + window.document.getElementById('btn-reset')?.click(); + await tick(); + assert.equal(select.value, ''); + assert.equal(select.disabled, false); + + window.document.getElementById('btn-fr')?.click(); + await tick(); + assert.equal(select.value, 'FR'); + assert.equal(select.disabled, true); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/main.svelte new file mode 100644 index 000000000000..45f2b129e81a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-block/main.svelte @@ -0,0 +1,41 @@ + + + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/_config.js new file mode 100644 index 000000000000..d4274870cad6 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + + + `, + + async test({ assert, component, window, logs }) { + // Primary assertion: No infinite loop error + assert.notInclude(logs, 'Infinite loop detected'); + + // Verify component state + const select = window.document.querySelector('select'); + if (!select) { + assert.fail('Select element not found'); + return; + } + + // With default_details fallback nothing is selected + assert.equal(select.value, ''); + assert.equal(select.disabled, false); + + window.document.getElementById('btn-us')?.click(); + await tick(); + assert.equal(select.disabled, true); + assert.equal(select.value, 'US'); + + window.document.getElementById('btn-reset')?.click(); + await tick(); + assert.equal(select.value, ''); + assert.equal(select.disabled, false); + + window.document.getElementById('btn-fr')?.click(); + await tick(); + assert.equal(select.value, 'FR'); + assert.equal(select.disabled, true); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/main.svelte new file mode 100644 index 000000000000..09b5dff734a9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-fallback/main.svelte @@ -0,0 +1,38 @@ + + + + + + + From 0ceac964255a9b471f108be9d54bbd9192e44e59 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 02:06:36 +0100 Subject: [PATCH 4/4] Add changeset --- .changeset/wild-queens-promise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-queens-promise.md diff --git a/.changeset/wild-queens-promise.md b/.changeset/wild-queens-promise.md new file mode 100644 index 000000000000..360b5f9eb4c9 --- /dev/null +++ b/.changeset/wild-queens-promise.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Improve compiler output on legacy components to prevent infinite loops from