From b0ca0b84dec787697d76fe077c7acca8e102ab01 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:25:14 +0100 Subject: [PATCH 001/273] fix: robustify blocker calculation (#17676) This was actually several bugs: - We used `scopes` for the blockers, that's actually the template scopes, should be `instance.scopes` instead - We missed setting the scope for `touch` - We didn't take return statements into account when calculating blockers. We cannot know when/if something within the return statement is called, so we gotta assume it is and touch everything transitively from it Combined this fixes #17667 (and possibly other cases not showing up in the issue tracker yet) Initially I just thought "ok I guess we have to traverse into functions, too" but then I thought that feels too unoptimized and came up with the return-statement-inspection, at which point I discovered the other bugs. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --- .changeset/true-cities-retire.md | 5 ++ .../src/compiler/phases/2-analyze/index.js | 61 ++++++++++++------- .../_config.js | 14 +++++ .../main.svelte | 41 +++++++++++++ 4 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 .changeset/true-cities-retire.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte diff --git a/.changeset/true-cities-retire.md b/.changeset/true-cities-retire.md new file mode 100644 index 0000000000..c1846e9267 --- /dev/null +++ b/.changeset/true-cities-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: robustify blocker calculation diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ef0b35f560..969af842cc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -690,7 +690,7 @@ export function analyze_component(root, source, options) { } } - calculate_blockers(instance, scopes, analysis); + calculate_blockers(instance, analysis); if (analysis.runes) { const props_refs = module.scope.references.get('$$props'); @@ -940,11 +940,10 @@ export function analyze_component(root, source, options) { * top level statements. This includes indirect blockers such as functions referencing async top level statements. * * @param {Js} instance - * @param {Map} scopes * @param {ComponentAnalysis} analysis * @returns {void} */ -function calculate_blockers(instance, scopes, analysis) { +function calculate_blockers(instance, analysis) { /** * @param {ESTree.Node} expression * @param {Scope} scope @@ -959,6 +958,14 @@ function calculate_blockers(instance, scopes, analysis) { expression, { scope }, { + _(node, context) { + const scope = instance.scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, ImportDeclaration(node) {}, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); @@ -979,14 +986,11 @@ function calculate_blockers(instance, scopes, analysis) { /** * @param {ESTree.Node} node - * @param {Set} seen * @param {Set} reads * @param {Set} writes + * @param {Scope} scope */ - const trace_references = (node, reads, writes, seen = new Set()) => { - if (seen.has(node)) return; - seen.add(node); - + const trace_references = (node, reads, writes, scope) => { /** * @param {ESTree.Pattern} node * @param {Scope} scope @@ -1005,10 +1009,10 @@ function calculate_blockers(instance, scopes, analysis) { walk( node, - { scope: instance.scope }, + { scope }, { _(node, context) { - const scope = scopes.get(node); + const scope = instance.scopes.get(node); if (scope) { context.next({ scope }); } else { @@ -1040,10 +1044,6 @@ function calculate_blockers(instance, scopes, analysis) { writes.add(b); } }, - // don't look inside functions until they are called - ArrowFunctionExpression(_, context) {}, - FunctionDeclaration(_, context) {}, - FunctionExpression(_, context) {}, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); if (is_reference(node, parent)) { @@ -1052,7 +1052,19 @@ function calculate_blockers(instance, scopes, analysis) { reads.add(binding); } } - } + }, + ReturnStatement(node, context) { + // We have to assume that anything returned from a function, even if it's a function itself, + // might be called immediately, so we have to touch all references within it. Example: + // function foo() { return () => blocker; } foo(); // blocker is touched + if (node.argument) { + touch(node.argument, context.state.scope, reads); + } + }, + // don't look inside functions until they are called + ArrowFunctionExpression(_, context) {}, + FunctionDeclaration(_, context) {}, + FunctionExpression(_, context) {} } ); }; @@ -1132,7 +1144,7 @@ function calculate_blockers(instance, scopes, analysis) { /** @type {Set} */ const writes = new Set(); - trace_references(declarator, reads, writes); + trace_references(declarator, reads, writes, instance.scope); const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) @@ -1160,7 +1172,7 @@ function calculate_blockers(instance, scopes, analysis) { /** @type {Set} */ const writes = new Set(); - trace_references(node, reads, writes); + trace_references(node, reads, writes, instance.scope); const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) @@ -1184,12 +1196,17 @@ function calculate_blockers(instance, scopes, analysis) { for (const fn of functions) { /** @type {Set} */ const reads_writes = new Set(); - const body = + const init = fn.type === 'VariableDeclarator' - ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body - : fn.body; - - trace_references(body, reads_writes, reads_writes); + ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init) + : fn; + + trace_references( + init.body, + reads_writes, + reads_writes, + /** @type {Scope} */ (instance.scopes.get(init)) + ); const max = [...reads_writes].reduce((max, binding) => { if (binding.blocker) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js new file mode 100644 index 0000000000..080e2d278c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: 'true true true true true', + + async test({ assert, target }) { + await new Promise((resolve) => setTimeout(resolve, 10)); + await tick(); + + assert.htmlEqual(target.innerHTML, 'true true true true true'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte new file mode 100644 index 0000000000..5f79a14830 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte @@ -0,0 +1,41 @@ + + + +{#if true} + {checkedFactory()()} +{/if} +{#if true} + {indirectCheckedFactory()()} +{/if} +{#if true} + {callFactory(checkedFactory)()} +{/if} +{#if true} + {indirectCallFactory()()} +{/if} +{#if true} + {indirectChecked2()()} +{/if} From 3c6bb6faba559d23dac95eb8cd1d10db190ee4a7 Mon Sep 17 00:00:00 2001 From: Manuel <30698007+manuel3108@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:36:54 +0100 Subject: [PATCH 002/273] chore: provide proper public type for `parseCss` result (#17654) Currently, the [newly introduced `parseCss` from `svelte/compiler`](https://github.com/sveltejs/svelte/pull/17496) returns `Omit`. If you try to work with this in external tooling, everywhere where you pass around the result of this method, you need to use that type as well, which is quite cumbersome. (I'm trying to integrate this into `sv` to get rid of a workaround) This creates a new type in the CSS AST to differentiate between one stylesheet only having the roles, and one beeing the full one that is used in `parse` itself. Im 100% open on the name of the new type or any better ideas. ### Before submitting the PR, please make sure you do the following - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- .changeset/tender-pugs-hide.md | 5 +++++ packages/svelte/src/compiler/index.js | 4 ++-- packages/svelte/src/compiler/types/css.d.ts | 11 +++++++++-- packages/svelte/tests/css-parse.test.ts | 6 +++--- packages/svelte/types/index.d.ts | 13 ++++++++++--- 5 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .changeset/tender-pugs-hide.md diff --git a/.changeset/tender-pugs-hide.md b/.changeset/tender-pugs-hide.md new file mode 100644 index 0000000000..260f49614e --- /dev/null +++ b/.changeset/tender-pugs-hide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: provide proper public type for `parseCss` result diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index acfef6a320..e864c4a1f4 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -123,7 +123,7 @@ export function parse(source, { modern, loose } = {}) { * The parseCss function parses a CSS stylesheet, returning its abstract syntax tree. * * @param {string} source The CSS source code - * @returns {Omit} + * @returns {AST.CSS.StyleSheetFile} */ export function parseCss(source) { source = remove_bom(source); @@ -135,7 +135,7 @@ export function parseCss(source) { const children = parse_stylesheet(parser); return { - type: 'StyleSheet', + type: 'StyleSheetFile', start: 0, end: source.length, children diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 154a06ffb1..13eb880d83 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -6,10 +6,17 @@ export namespace _CSS { end: number; } - export interface StyleSheet extends BaseNode { + export interface StyleSheetBase extends BaseNode { + children: Array; + } + + export interface StyleSheetFile extends StyleSheetBase { + type: 'StyleSheetFile'; + } + + export interface StyleSheet extends StyleSheetBase { type: 'StyleSheet'; attributes: any[]; // TODO - children: Array; content: { start: number; end: number; diff --git a/packages/svelte/tests/css-parse.test.ts b/packages/svelte/tests/css-parse.test.ts index 4d8ef8601b..3a53e97955 100644 --- a/packages/svelte/tests/css-parse.test.ts +++ b/packages/svelte/tests/css-parse.test.ts @@ -4,7 +4,7 @@ import { parseCss } from 'svelte/compiler'; describe('parseCss', () => { it('parses a simple rule', () => { const ast = parseCss('div { color: red; }'); - assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.type, 'StyleSheetFile'); assert.equal(ast.children.length, 1); assert.equal(ast.children[0].type, 'Rule'); }); @@ -57,7 +57,7 @@ describe('parseCss', () => { it('parses empty stylesheet', () => { const ast = parseCss(''); - assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.type, 'StyleSheetFile'); assert.equal(ast.children.length, 0); assert.equal(ast.start, 0); assert.equal(ast.end, 0); @@ -138,7 +138,7 @@ describe('parseCss', () => { it('parses escaped characters', () => { const ast = parseCss("div { background: url('./example.png?\\''); }"); - assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.type, 'StyleSheetFile'); assert.equal(ast.children.length, 1); const rule = ast.children[0]; assert.equal(rule.type, 'Rule'); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 62c0e210be..e7aa5395b8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -889,7 +889,7 @@ declare module 'svelte/compiler' { * * @param source The CSS source code * */ - export function parseCss(source: string): Omit; + export function parseCss(source: string): AST.CSS.StyleSheetFile; /** * @deprecated Replace this with `import { walk } from 'estree-walker'` * */ @@ -1673,10 +1673,17 @@ declare module 'svelte/compiler' { end: number; } - export interface StyleSheet extends BaseNode { + export interface StyleSheetBase extends BaseNode { + children: Array; + } + + export interface StyleSheetFile extends StyleSheetBase { + type: 'StyleSheetFile'; + } + + export interface StyleSheet extends StyleSheetBase { type: 'StyleSheet'; attributes: any[]; // TODO - children: Array; content: { start: number; end: number; From 684cdba2538bd7cfc695fcd8516ecbd21b3c08b5 Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:53:23 +0100 Subject: [PATCH 003/273] fix: resolve effect_update_depth_exceeded with select bind:value in legacy mode (#17645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #13768 `` elements, and call `invalidate_inner_signals` inline at the mutation point in `AssignmentExpression` — only when the binding is actually mutated, avoiding the read-write cycle. Based on the approach outlined in #16200. ## Changes - **`scope.js`**: Add `legacy_indirect_bindings` field to `Binding` class - **`RegularElement.js` (analyze)**: For `` with derived state in legacy mode diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index c7b40109a3..bc65bd65db 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -16,6 +16,8 @@ import { regex_starts_with_newline } from '../../patterns.js'; import { check_element } from './shared/a11y/index.js'; import { validate_element } from './shared/element.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; +import { object } from '../../../utils/ast.js'; +import { runes } from '../../../state.js'; /** * @param {AST.RegularElement} node @@ -64,6 +66,34 @@ export function RegularElement(node, context) { } } + // Special case: ` + + + + + `, + + async test({ assert, target, window, variant }) { + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + + const [select] = target.querySelectorAll('select'); + const options = target.querySelectorAll('option'); + + assert.equal(select.value, ''); + + const change = new window.Event('change'); + + // Select "UK" + options[2].selected = true; + await select.dispatchEvent(change); + + assert.equal(select.value, 'uk'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-derived/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-derived/main.svelte new file mode 100644 index 0000000000..57342347f5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-reactive-derived/main.svelte @@ -0,0 +1,21 @@ + + + From 015e744962e46a13011f8c7e3cdd4db7c9f742ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:08:12 -0500 Subject: [PATCH 004/273] Version Packages (#17668) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.50.2 ### Patch Changes - fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `` with derived state in legacy mode diff --git a/.changeset/orange-wasps-visit.md b/.changeset/orange-wasps-visit.md deleted file mode 100644 index 972ff636b8..0000000000 --- a/.changeset/orange-wasps-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused` diff --git a/.changeset/tender-pugs-hide.md b/.changeset/tender-pugs-hide.md deleted file mode 100644 index 260f49614e..0000000000 --- a/.changeset/tender-pugs-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: provide proper public type for `parseCss` result diff --git a/.changeset/true-cities-retire.md b/.changeset/true-cities-retire.md deleted file mode 100644 index c1846e9267..0000000000 --- a/.changeset/true-cities-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: robustify blocker calculation diff --git a/.changeset/wild-dolls-hang.md b/.changeset/wild-dolls-hang.md deleted file mode 100644 index a7b3436d69..0000000000 --- a/.changeset/wild-dolls-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reduce if block nesting diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3fcc8edfc1..5882398fb0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.50.2 + +### Patch Changes + +- fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `