From 2b49a5e3c0851ddba87d78b48551bd5f40e1ee13 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 5 Oct 2025 17:59:58 -0400 Subject: [PATCH 01/34] fix: flush when pending boundaries resolve (#16897) * fix: flush when pending boundaries resolve * note to self * Update packages/svelte/src/internal/client/dom/blocks/boundary.js --- .changeset/swift-starfishes-knock.md | 5 +++++ .../svelte/src/internal/client/dom/blocks/boundary.js | 7 +++++++ .../samples/async-attachment-in-block/_config.js | 11 +++++++++++ .../samples/async-attachment-in-block/main.svelte | 9 +++++++++ 4 files changed, 32 insertions(+) create mode 100644 .changeset/swift-starfishes-knock.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/main.svelte diff --git a/.changeset/swift-starfishes-knock.md b/.changeset/swift-starfishes-knock.md new file mode 100644 index 0000000000..790b3fe697 --- /dev/null +++ b/.changeset/swift-starfishes-knock.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush when pending boundaries resolve diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 6dde40d889..88f4a85e20 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -285,6 +285,13 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } + + // TODO this feels like a little bit of a kludge, but until we + // overhaul the boundary/batch relationship it's probably + // the most pragmatic solution available to us + queue_micro_task(() => { + Batch.ensure().flush(); + }); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/_config.js new file mode 100644 index 0000000000..97da1bf835 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '
attachment ran
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/main.svelte new file mode 100644 index 0000000000..027b980bd1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment-in-block/main.svelte @@ -0,0 +1,9 @@ +{#if await true} +
{ + node.textContent = 'attachment ran'; + }} + > + attachment did not run +
+{/if} From acdd93053da6a34aef104521ab87e85fe78081b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:11:05 -0400 Subject: [PATCH 02/34] Version Packages (#16899) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/swift-starfishes-knock.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/swift-starfishes-knock.md diff --git a/.changeset/swift-starfishes-knock.md b/.changeset/swift-starfishes-knock.md deleted file mode 100644 index 790b3fe697..0000000000 --- a/.changeset/swift-starfishes-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush when pending boundaries resolve diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 18a726277f..13a2b4cc4d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.39.9 + +### Patch Changes + +- fix: flush when pending boundaries resolve ([#16897](https://github.com/sveltejs/svelte/pull/16897)) + ## 5.39.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 21752e2d4b..7a616abbe8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.8", + "version": "5.39.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 6d69b57447..efc02968d7 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.8'; +export const VERSION = '5.39.9'; export const PUBLIC_VERSION = '5'; From fc39f2ed0d2dd60bf1acd49fefb5c4e934d88b09 Mon Sep 17 00:00:00 2001 From: "Dominik G." Date: Mon, 6 Oct 2025 18:23:59 +0200 Subject: [PATCH 03/34] chore: port security improvements from vite-ecosystem-ci trigger workflow in vite repo (#16902) --- .github/workflows/ecosystem-ci-trigger.yml | 45 ++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index aa08df2f84..8a6d1bf345 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -11,14 +11,13 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') permissions: - issues: write # to add / delete reactions + issues: write # to add / delete reactions, post comments pull-requests: write # to read PR data, and to add labels actions: read # to check workflow status contents: read # to clone the repo steps: - - name: monitor action permissions - - name: check user authorization # user needs triage permission - uses: actions/github-script@v7 + - name: Check User Permissions + uses: actions/github-script@v8 id: check-permissions with: script: | @@ -57,7 +56,7 @@ jobs: } - name: Get PR Data - uses: actions/github-script@v7 + uses: actions/github-script@v8 id: get-pr-data with: script: | @@ -67,6 +66,37 @@ jobs: repo: context.repo.repo, pull_number: context.issue.number }) + + const commentCreatedAt = new Date(context.payload.comment.created_at) + const commitPushedAt = new Date(pr.head.repo.pushed_at) + + console.log(`Comment created at: ${commentCreatedAt.toISOString()}`) + console.log(`PR last pushed at: ${commitPushedAt.toISOString()}`) + + // Check if any commits were pushed after the comment was created + if (commitPushedAt > commentCreatedAt) { + const errorMsg = [ + '⚠️ Security warning: PR was updated after the trigger command was posted.', + '', + `Comment posted at: ${commentCreatedAt.toISOString()}`, + `PR last pushed at: ${commitPushedAt.toISOString()}`, + '', + 'This could indicate an attempt to inject code after approval.', + 'Please review the latest changes and re-run /ecosystem-ci run if they are acceptable.' + ].join('\n') + + core.setFailed(errorMsg) + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: errorMsg + }) + + throw new Error('PR was pushed to after comment was created') + } + return { num: context.issue.number, branchName: pr.head.ref, @@ -85,15 +115,16 @@ jobs: svelte-ecosystem-ci - name: Trigger Downstream Workflow - uses: actions/github-script@v7 + uses: actions/github-script@v8 id: trigger env: COMMENT: ${{ github.event.comment.body }} + PR_DATA: ${{ steps.get-pr-data.outputs.result }} with: github-token: ${{ steps.generate-token.outputs.token }} script: | const comment = process.env.COMMENT.trim() - const prData = ${{ steps.get-pr-data.outputs.result }} + const prData = JSON.parse(process.env.PR_DATA) const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim() From 303750a1241ac0ded3115ff879fa417e23fe7c89 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:21:43 -0700 Subject: [PATCH 04/34] fix: don't replace rest props with `$$props` for excluded props (#16898) Closes #16895 --- .changeset/nervous-flies-laugh.md | 5 +++++ .../2-analyze/visitors/VariableDeclarator.js | 15 +++++++++++++++ .../3-transform/client/visitors/Identifier.js | 6 +++++- packages/svelte/src/compiler/phases/scope.js | 2 +- .../rest-props-excludes-properties/_config.js | 7 +++++++ .../component.svelte | 4 ++++ .../rest-props-excludes-properties/main.svelte | 4 ++++ 7 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/nervous-flies-laugh.md create mode 100644 packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/main.svelte diff --git a/.changeset/nervous-flies-laugh.md b/.changeset/nervous-flies-laugh.md new file mode 100644 index 0000000000..88c7694bcd --- /dev/null +++ b/.changeset/nervous-flies-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't replace rest props with `$$props` for excluded props diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index f56a665de8..7a85b4a93a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -46,6 +46,21 @@ export function VariableDeclarator(node, context) { : path.is_rest ? 'rest_prop' : 'prop'; + if (rune === '$props' && binding.kind === 'rest_prop' && node.id.type === 'ObjectPattern') { + const { properties } = node.id; + /** @type {string[]} */ + const exclude_props = []; + for (const property of properties) { + if (property.type === 'RestElement') { + continue; + } + const key = /** @type {Identifier | Literal & { value: string | number }} */ ( + property.key + ); + exclude_props.push(key.type === 'Identifier' ? key.name : key.value.toString()); + } + (binding.metadata ??= {}).exclude_props = exclude_props; + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js index b01ed01bd7..b43ec7891e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js @@ -32,7 +32,11 @@ export function Identifier(node, context) { grand_parent?.type !== 'AssignmentExpression' && grand_parent?.type !== 'UpdateExpression' ) { - return b.id('$$props'); + const key = /** @type {Identifier} */ (parent.property); + + if (!binding.metadata?.exclude_props?.includes(key.name)) { + return b.id('$$props'); + } } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0c6b64dd04..ffccaffba3 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -122,7 +122,7 @@ export class Binding { /** * Additional metadata, varies per binding type - * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }} + * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean; exclude_props?: string[] }} */ metadata = null; diff --git a/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/_config.js b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/_config.js new file mode 100644 index 0000000000..66a42c08db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.equal(target.textContent, ' false'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/component.svelte b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/component.svelte new file mode 100644 index 0000000000..40561218db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/component.svelte @@ -0,0 +1,4 @@ + +{rest.name} {'name' in rest} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/main.svelte b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/main.svelte new file mode 100644 index 0000000000..d9f6d8a21c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/rest-props-excludes-properties/main.svelte @@ -0,0 +1,4 @@ + + \ No newline at end of file From 5d1ffcf413aa475dad2bdf975e907e2958615342 Mon Sep 17 00:00:00 2001 From: Yuki Ishii Date: Tue, 7 Oct 2025 23:27:30 +0900 Subject: [PATCH 05/34] fix: allow await in if block consequent and alternate (#16890) Closes #16885 --- .changeset/kind-tigers-retire.md | 5 +++++ .../phases/3-transform/server/visitors/IfBlock.js | 6 +++++- .../samples/async-if-const/_expected.html | 1 + .../samples/async-if-const/main.svelte | 10 ++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/kind-tigers-retire.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-const/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-const/main.svelte diff --git a/.changeset/kind-tigers-retire.md b/.changeset/kind-tigers-retire.md new file mode 100644 index 0000000000..432ff1858b --- /dev/null +++ b/.changeset/kind-tigers-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow await in if block consequent and alternate diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 798cb02e49..a869a8edf1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -23,7 +23,11 @@ export function IfBlock(node, context) { /** @type {Statement} */ let statement = b.if(test, consequent, alternate); - if (node.metadata.expression.has_await) { + if ( + node.metadata.expression.has_await || + node.consequent.metadata.has_await || + node.alternate?.metadata.has_await + ) { statement = create_async_block(b.block([statement])); } diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-const/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-if-const/_expected.html new file mode 100644 index 0000000000..e440e5c842 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-const/_expected.html @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-const/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-if-const/main.svelte new file mode 100644 index 0000000000..1437ac8f14 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-if-const/main.svelte @@ -0,0 +1,10 @@ +{#if false} + {@const one = await 1} + {one} +{:else if false} + {@const two = await 2} + {two} +{:else} + {@const three = await 3} + {three} +{/if} From edcd4b59e3d06b14e8b5bc216b7e8ea874dd4995 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:29:34 -0700 Subject: [PATCH 06/34] fix: correctly transform `$derived` private fields on server (#16894) Closes #16889 --- .changeset/seven-flies-drop.md | 5 +++++ .../phases/3-transform/server/visitors/MemberExpression.js | 6 +----- .../samples/class-state-derived-private/main.svelte | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 .changeset/seven-flies-drop.md diff --git a/.changeset/seven-flies-drop.md b/.changeset/seven-flies-drop.md new file mode 100644 index 0000000000..fed9c7dcac --- /dev/null +++ b/.changeset/seven-flies-drop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform `$derived` private fields on server diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js index 50b5ae793f..6326e9efe2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js @@ -7,11 +7,7 @@ import * as b from '#compiler/builders'; * @param {Context} context */ export function MemberExpression(node, context) { - if ( - context.state.analysis.runes && - node.object.type === 'ThisExpression' && - node.property.type === 'PrivateIdentifier' - ) { + if (context.state.analysis.runes && node.property.type === 'PrivateIdentifier') { const field = context.state.state_fields?.get(`#${node.property.name}`); if (field?.type === '$derived' || field?.type === '$derived.by') { diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte index d971566396..92a1f5ab94 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte @@ -9,7 +9,8 @@ } get embiggened1() { - return this.#doubled; + const self = this; + return self.#doubled; } get embiggened2() { From 1b1f14439660fed4df5074b9a648e9a1858e3b26 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 7 Oct 2025 17:11:39 +0200 Subject: [PATCH 07/34] fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` (#16910) --- .changeset/tall-parents-go.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 1 + .../const-snippet-reactive/Component.svelte | 5 +++++ .../samples/const-snippet-reactive/_config.js | 10 ++++++++++ .../samples/const-snippet-reactive/main.svelte | 15 +++++++++++++++ 5 files changed, 36 insertions(+) create mode 100644 .changeset/tall-parents-go.md create mode 100644 packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/main.svelte diff --git a/.changeset/tall-parents-go.md b/.changeset/tall-parents-go.md new file mode 100644 index 0000000000..6146a5c675 --- /dev/null +++ b/.changeset/tall-parents-go.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index ffccaffba3..f7d3dac0f7 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -263,6 +263,7 @@ class Evaluation { if (binding.initial?.type === 'SnippetBlock') { this.is_defined = true; this.is_known = false; + this.values.add(UNKNOWN); break; } diff --git a/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/Component.svelte b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/Component.svelte new file mode 100644 index 0000000000..ba0e19e0a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/Component.svelte @@ -0,0 +1,5 @@ + + +{@render test?.()} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/_config.js new file mode 100644 index 0000000000..5f6a0172ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/_config.js @@ -0,0 +1,10 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, `

snip

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/main.svelte new file mode 100644 index 0000000000..c2b75c362f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-snippet-reactive/main.svelte @@ -0,0 +1,15 @@ + + +{#snippet snip()} +

snip

+{/snippet} + + +{#if true} + {@const test = count % 2 === 0 ? undefined: snip} + +{/if} + From 78481862fb033bfaa1c31d84eba96460b0dfe19e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:00:05 +0200 Subject: [PATCH 08/34] fix: hydrate each blocks inside element correctly (#16908) We have an each block optimization that omits the comment when the each block is the sole child of an element. This optimization clashes with async which wants to skip ahead to the sibling closing comment. For now we therefore remove that optimization when the each block is async. In the long run we should instead optimize _all_ cases where _any_ block is the sole child of an element, in both async and sync mode, consistently. fixes #16905 fixes #16907 --- .changeset/chilly-bats-build.md | 5 +++++ .../client/visitors/shared/fragment.js | 9 ++++++++- .../samples/async-each-sibling/_config.js | 16 ++++++++++++++++ .../samples/async-each-sibling/main.svelte | 11 +++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .changeset/chilly-bats-build.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-sibling/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-sibling/main.svelte diff --git a/.changeset/chilly-bats-build.md b/.changeset/chilly-bats-build.md new file mode 100644 index 0000000000..872e4c79c4 --- /dev/null +++ b/.changeset/chilly-bats-build.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: hydrate each blocks inside element correctly diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 62d07014ee..3588f2843a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -99,7 +99,14 @@ export function process_children(nodes, initial, is_element, context) { if (is_static_element(node, context.state)) { skipped += 1; - } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { + } else if ( + node.type === 'EachBlock' && + nodes.length === 1 && + is_element && + // In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled + // TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally) + !(node.body.metadata.has_await || node.metadata.expression.has_await) + ) { node.metadata.is_controlled = true; } else { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node'); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-sibling/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-sibling/_config.js new file mode 100644 index 0000000000..ff10a48f11 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-sibling/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'hydrate', 'client'], + ssrHtml: `
  • 1
`, + + async test({ assert, target }) { + await tick(); + const [add] = target.querySelectorAll('button'); + + add.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `
  • 1
  • 2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-sibling/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-sibling/main.svelte new file mode 100644 index 0000000000..2c35440511 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-sibling/main.svelte @@ -0,0 +1,11 @@ + + +
    +{#each await array as item} +
  • {item}
  • +{/each} +
+ + From 06bd6a8fbdd1aada619fe68114f3a6cc8ef37c4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:03:07 -0700 Subject: [PATCH 09/34] Version Packages (#16909) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/chilly-bats-build.md | 5 ----- .changeset/kind-tigers-retire.md | 5 ----- .changeset/nervous-flies-laugh.md | 5 ----- .changeset/seven-flies-drop.md | 5 ----- .changeset/tall-parents-go.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/chilly-bats-build.md delete mode 100644 .changeset/kind-tigers-retire.md delete mode 100644 .changeset/nervous-flies-laugh.md delete mode 100644 .changeset/seven-flies-drop.md delete mode 100644 .changeset/tall-parents-go.md diff --git a/.changeset/chilly-bats-build.md b/.changeset/chilly-bats-build.md deleted file mode 100644 index 872e4c79c4..0000000000 --- a/.changeset/chilly-bats-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: hydrate each blocks inside element correctly diff --git a/.changeset/kind-tigers-retire.md b/.changeset/kind-tigers-retire.md deleted file mode 100644 index 432ff1858b..0000000000 --- a/.changeset/kind-tigers-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow await in if block consequent and alternate diff --git a/.changeset/nervous-flies-laugh.md b/.changeset/nervous-flies-laugh.md deleted file mode 100644 index 88c7694bcd..0000000000 --- a/.changeset/nervous-flies-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't replace rest props with `$$props` for excluded props diff --git a/.changeset/seven-flies-drop.md b/.changeset/seven-flies-drop.md deleted file mode 100644 index fed9c7dcac..0000000000 --- a/.changeset/seven-flies-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly transform `$derived` private fields on server diff --git a/.changeset/tall-parents-go.md b/.changeset/tall-parents-go.md deleted file mode 100644 index 6146a5c675..0000000000 --- a/.changeset/tall-parents-go.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 13a2b4cc4d..84902a4718 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.39.10 + +### Patch Changes + +- fix: hydrate each blocks inside element correctly ([#16908](https://github.com/sveltejs/svelte/pull/16908)) + +- fix: allow await in if block consequent and alternate ([#16890](https://github.com/sveltejs/svelte/pull/16890)) + +- fix: don't replace rest props with `$$props` for excluded props ([#16898](https://github.com/sveltejs/svelte/pull/16898)) + +- fix: correctly transform `$derived` private fields on server ([#16894](https://github.com/sveltejs/svelte/pull/16894)) + +- fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` ([#16910](https://github.com/sveltejs/svelte/pull/16910)) + ## 5.39.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7a616abbe8..41c43cdd25 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.9", + "version": "5.39.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index efc02968d7..cd2c8e9440 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.9'; +export const VERSION = '5.39.10'; export const PUBLIC_VERSION = '5'; From b5c84371510d39770243a4431e271dd2ae5b2289 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 8 Oct 2025 06:58:21 -0400 Subject: [PATCH 10/34] fix: flush batches whenever an async value resolves (#16912) * fix: flush batches whenever an async value resolves * move some code around * unnecessary --- .changeset/metal-parents-train.md | 5 +++ .../src/internal/client/reactivity/async.js | 2 - .../src/internal/client/reactivity/batch.js | 45 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 .changeset/metal-parents-train.md diff --git a/.changeset/metal-parents-train.md b/.changeset/metal-parents-train.md new file mode 100644 index 0000000000..57c99453c3 --- /dev/null +++ b/.changeset/metal-parents-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush batches whenever an async value resolves diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 4d572281b7..45c78ff926 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -52,8 +52,6 @@ export function flatten(sync, async, fn) { Promise.all(async.map((expression) => async_derived(expression))) .then((result) => { - batch?.activate(); - restore(); try { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fb704edb13..102d0670b6 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -178,6 +178,8 @@ export class Batch { flush_queued_effects(render_effects); flush_queued_effects(effects); + previous_batch = null; + this.#deferred?.resolve(); } else { this.#defer_effects(this.#render_effects); @@ -280,17 +282,6 @@ export class Batch { deactivate() { current_batch = null; - previous_batch = null; - - for (const update of effect_pending_updates) { - effect_pending_updates.delete(update); - update(); - - if (current_batch !== null) { - // only do one at a time - break; - } - } } flush() { @@ -307,6 +298,16 @@ export class Batch { } this.deactivate(); + + for (const update of effect_pending_updates) { + effect_pending_updates.delete(update); + update(); + + if (current_batch !== null) { + // only do one at a time + break; + } + } } /** @@ -375,21 +376,17 @@ export class Batch { decrement() { this.#pending -= 1; - if (this.#pending === 0) { - for (const e of this.#dirty_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#maybe_dirty_effects) { - set_signal_status(e, MAYBE_DIRTY); - schedule_effect(e); - } + for (const e of this.#dirty_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } - this.flush(); - } else { - this.deactivate(); + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); } + + this.flush(); } /** @param {() => void} fn */ From 844fab7798dd242bed0460ceeebd18c50674f83b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:01:06 +0200 Subject: [PATCH 11/34] Version Packages (#16913) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/metal-parents-train.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/metal-parents-train.md diff --git a/.changeset/metal-parents-train.md b/.changeset/metal-parents-train.md deleted file mode 100644 index 57c99453c3..0000000000 --- a/.changeset/metal-parents-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush batches whenever an async value resolves diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 84902a4718..2302acd033 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.39.11 + +### Patch Changes + +- fix: flush batches whenever an async value resolves ([#16912](https://github.com/sveltejs/svelte/pull/16912)) + ## 5.39.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 41c43cdd25..822f19f001 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.10", + "version": "5.39.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cd2c8e9440..0f723c4eb0 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.10'; +export const VERSION = '5.39.11'; export const PUBLIC_VERSION = '5'; From 9c350cfdab1a55e7d7960e434e2a3349e9da0946 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:59:40 +0200 Subject: [PATCH 12/34] chore: test for batches flush fix (#16915) Test for #16912 Also some explanation what the bug was: 1. async batch kicks off 2. outer async work succeeds, still something pending, so doesn't do anything for now 3. something unrelated writes to a signal (in the remote functions case it's the query writing to loading, raw etc), which creates a new batch 4. new batch executes. since there are multiple batches it takes the previous value which means if block is still alive. commits that, since no async work from the perspective of this branch 5. inner async work succeeds. now the batch has zero pending async work so it can flush. But the if block is no longer dirty since it was done by the other batch already -> never undos the other work #16912 fixes it by still traversing the tree which means the if block deletion is scheduled to commit later, which it then does --- .../async-inner-after-outer/_config.js | 57 +++++++++++++++++++ .../async-inner-after-outer/main.svelte | 41 +++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/_config.js b/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/_config.js new file mode 100644 index 0000000000..8905ee4bf5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/_config.js @@ -0,0 +1,57 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const shift = document.querySelector('button'); + shift?.click(); + await tick(); + shift?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

true

+ + + ` + ); + + const toggle = target.querySelector('button'); + toggle?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

true

+ + + ` + ); + + shift?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

true

+ + + ` + ); + + shift?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/main.svelte new file mode 100644 index 0000000000..0b3b21f28c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-inner-after-outer/main.svelte @@ -0,0 +1,41 @@ + + + + {#if await foo()} +

{await bar()}

+ {/if} + + + + {#snippet pending()} +

loading...

+ {/snippet} +
+ + \ No newline at end of file From 7f8e60fd8a53e1eed28a60b5514bd07b49e97423 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 9 Oct 2025 13:32:45 +0200 Subject: [PATCH 13/34] fix: generate correct SSR code for the case where `pending` is an attribute (#16919) Closes #16886 --- .changeset/spicy-rabbits-drive.md | 5 +++++ .../phases/3-transform/server/visitors/SvelteBoundary.js | 7 +++++-- .../samples/boundary-pending-prop-async/_config.js | 3 +++ .../samples/boundary-pending-prop-async/_expected.html | 1 + .../samples/boundary-pending-prop-async/main.svelte | 8 ++++++++ 5 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .changeset/spicy-rabbits-drive.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/main.svelte diff --git a/.changeset/spicy-rabbits-drive.md b/.changeset/spicy-rabbits-drive.md new file mode 100644 index 0000000000..01834294e1 --- /dev/null +++ b/.changeset/spicy-rabbits-drive.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: generate correct SSR code for the case where `pending` is an attribute diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index b272870ac3..41a18cf52d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -43,11 +43,14 @@ export function SvelteBoundary(node, context) { ); const pending = b.call(callee, b.id('$$renderer')); const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); + const statement = node.fragment.metadata.has_await + ? create_async_block(b.block([block])) + : block; context.state.template.push( b.if( callee, - b.block(build_template([block_open_else, pending, block_close])), - b.block(build_template([block_open, block, block_close])) + b.block([b.stmt(pending)]), + b.block(build_template([block_open, statement, block_close])) ) ); } else { diff --git a/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_config.js b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_expected.html b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_expected.html new file mode 100644 index 0000000000..ac7cd985f1 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/_expected.html @@ -0,0 +1 @@ +Loading... \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/main.svelte b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/main.svelte new file mode 100644 index 0000000000..7e0d4c62ea --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/boundary-pending-prop-async/main.svelte @@ -0,0 +1,8 @@ +{#snippet pending()} + Loading... +{/snippet} + + + {@const data = await Promise.resolve('hello')} +

{data}

+
\ No newline at end of file From 297afd057828a1bf04f15d5b22b4123fd3e522d8 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 9 Oct 2025 18:43:41 +0200 Subject: [PATCH 14/34] fix: generate correct code for `each` blocks with async body (#16923) * fix: generate correct code for `each` blocks with async body * fix: else branch of `each` block * chore: add expected html --- .changeset/wicked-goats-begin.md | 5 +++++ .../phases/3-transform/server/visitors/EachBlock.js | 6 ++++-- .../samples/each-body-async/_config.js | 5 +++++ .../samples/each-body-async/_expected.html | 1 + .../samples/each-body-async/main.svelte | 11 +++++++++++ 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/wicked-goats-begin.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/each-body-async/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/each-body-async/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/each-body-async/main.svelte diff --git a/.changeset/wicked-goats-begin.md b/.changeset/wicked-goats-begin.md new file mode 100644 index 0000000000..04a22aa310 --- /dev/null +++ b/.changeset/wicked-goats-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: generate correct code for `each` blocks with async body diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index 5a7ca8b566..2d5b0c8931 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -32,7 +32,9 @@ export function EachBlock(node, context) { each.push(b.let(node.index, index)); } - each.push(.../** @type {BlockStatement} */ (context.visit(node.body)).body); + const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body; + + each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)); const for_loop = b.for( b.declaration('let', [ @@ -55,7 +57,7 @@ export function EachBlock(node, context) { b.if( b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.block([open, for_loop]), - fallback + node.fallback.metadata.has_await ? create_async_block(fallback) : fallback ) ); } else { diff --git a/packages/svelte/tests/server-side-rendering/samples/each-body-async/_config.js b/packages/svelte/tests/server-side-rendering/samples/each-body-async/_config.js new file mode 100644 index 0000000000..05de37a8bd --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/each-body-async/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'] +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/each-body-async/_expected.html b/packages/svelte/tests/server-side-rendering/samples/each-body-async/_expected.html new file mode 100644 index 0000000000..605299c2ae --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/each-body-async/_expected.html @@ -0,0 +1 @@ +each else \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/each-body-async/main.svelte b/packages/svelte/tests/server-side-rendering/samples/each-body-async/main.svelte new file mode 100644 index 0000000000..86e15c4e7e --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/each-body-async/main.svelte @@ -0,0 +1,11 @@ +{#each { length: 1 }} + {@const data = await Promise.resolve("each")} + {data} +{/each} + +{#each { length: 0 }} + should not see this +{:else} + {@const data = await Promise.resolve("else")} + {data} +{/each} \ No newline at end of file From 93012e1e6fce4d26b75d065301330cbe8a9e489a Mon Sep 17 00:00:00 2001 From: 7nik Date: Thu, 9 Oct 2025 23:26:52 +0300 Subject: [PATCH 15/34] fix: track the user's getter of `bind:this` (#16916) --- .changeset/rude-frogs-train.md | 5 ++ .../docs/03-template-syntax/12-bind.md | 2 + .../client/visitors/AssignmentExpression.js | 11 +++-- .../client/visitors/shared/utils.js | 48 +++++++++++++------ .../bind-getter-setter-loop/_config.js | 16 +++++++ .../bind-getter-setter-loop/main.svelte | 13 +++++ 6 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 .changeset/rude-frogs-train.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte diff --git a/.changeset/rude-frogs-train.md b/.changeset/rude-frogs-train.md new file mode 100644 index 0000000000..06da5dcc1e --- /dev/null +++ b/.changeset/rude-frogs-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: track the user's getter of `bind:this` diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md index c21ed35919..be84969b87 100644 --- a/documentation/docs/03-template-syntax/12-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -364,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins ``` +> [!NOTE] In case of using [the function bindings](#Function-bindings), the getter is required to ensure that the correct value is nullified on component or element destruction. + ## bind:_property_ for components ```svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 7d64d60bca..731569aaae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -192,17 +192,18 @@ function build_assignment(operator, left, right, context) { path.at(-1) === 'Component' || path.at(-1) === 'SvelteComponent' || (path.at(-1) === 'ArrowFunctionExpression' && - path.at(-2) === 'SequenceExpression' && - (path.at(-3) === 'Component' || - path.at(-3) === 'SvelteComponent' || - path.at(-3) === 'BindDirective')) + (path.at(-2) === 'BindDirective' || + (path.at(-2) === 'Component' && path.at(-3) === 'Fragment') || + (path.at(-2) === 'SequenceExpression' && + (path.at(-3) === 'Component' || + path.at(-3) === 'SvelteComponent' || + path.at(-3) === 'BindDirective')))) ) { should_transform = false; } if (left.type === 'MemberExpression' && should_transform) { const callee = callees[operator]; - return /** @type {Expression} */ ( context.visit( b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ba140a153e..a42063b2e2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -209,10 +209,8 @@ export function parse_directive_name(name) { * @param {import('zimmerframe').Context} context */ export function build_bind_this(expression, value, { state, visit }) { - if (expression.type === 'SequenceExpression') { - const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; - return b.call('$.bind_this', value, set, get); - } + const [getter, setter] = + expression.type === 'SequenceExpression' ? expression.expressions : [null, null]; /** @type {Identifier[]} */ const ids = []; @@ -229,7 +227,7 @@ export function build_bind_this(expression, value, { state, visit }) { // Note that we only do this for each context variables, the consequence is that the value might be stale in // some scenarios where the value is a member expression with changing computed parts or using a combination of multiple // variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this. - walk(expression, null, { + walk(getter ?? expression, null, { Identifier(node, { path }) { if (seen.includes(node.name)) return; seen.push(node.name); @@ -260,9 +258,17 @@ export function build_bind_this(expression, value, { state, visit }) { const child_state = { ...state, transform }; - const get = /** @type {Expression} */ (visit(expression, child_state)); - const set = /** @type {Expression} */ ( - visit(b.assignment('=', expression, b.id('$$value')), child_state) + let get = /** @type {Expression} */ (visit(getter ?? expression, child_state)); + let set = /** @type {Expression} */ ( + visit( + setter ?? + b.assignment( + '=', + /** @type {Identifier | MemberExpression} */ (expression), + b.id('$$value') + ), + child_state + ) ); // If we're mutating a property, then it might already be non-existent. @@ -275,13 +281,25 @@ export function build_bind_this(expression, value, { state, visit }) { node = node.object; } - return b.call( - '$.bind_this', - value, - b.arrow([b.id('$$value'), ...ids], set), - b.arrow([...ids], get), - values.length > 0 && b.thunk(b.array(values)) - ); + get = + get.type === 'ArrowFunctionExpression' + ? b.arrow([...ids], get.body) + : get.type === 'FunctionExpression' + ? b.function(null, [...ids], get.body) + : getter + ? get + : b.arrow([...ids], get); + + set = + set.type === 'ArrowFunctionExpression' + ? b.arrow([set.params[0] ?? b.id('_'), ...ids], set.body) + : set.type === 'FunctionExpression' + ? b.function(null, [set.params[0] ?? b.id('_'), ...ids], set.body) + : setter + ? set + : b.arrow([b.id('$$value'), ...ids], set); + + return b.call('$.bind_this', value, set, get, values.length > 0 && b.thunk(b.array(values))); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js new file mode 100644 index 0000000000..9c8b4fc6c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [btn] = target.querySelectorAll('button'); + + flushSync(() => { + btn.click(); + }); + assert.htmlEqual( + target.innerHTML, + '
51423
51423' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte new file mode 100644 index 0000000000..60444e8978 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-loop/main.svelte @@ -0,0 +1,13 @@ + + +
+{#each arr as item, i (item)} + elements[i], (v) => elements[i] = v }>{item} +{/each} +
+{#each elements as elem} + {elem.textContent} +{/each} \ No newline at end of file From 5d9b4c66ca4df817c0c8146b71d30fbbdb958872 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:25:35 +0200 Subject: [PATCH 16/34] fix: better input cursor restoration for `bind:value` (#16925) If cursor was at end and new input is longer, move cursor to new end No test because not possible to reproduce using our test setup. Follow-up to #14649, helps with #16577 --- .changeset/fair-aliens-wait.md | 5 +++++ .../internal/client/dom/elements/bindings/input.js | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .changeset/fair-aliens-wait.md diff --git a/.changeset/fair-aliens-wait.md b/.changeset/fair-aliens-wait.md new file mode 100644 index 0000000000..66e4cf2bd1 --- /dev/null +++ b/.changeset/fair-aliens-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better input cursor restoration for `bind:value` diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 815acde7c5..23ad6f5cdc 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -43,14 +43,22 @@ export function bind_value(input, get, set = get) { if (value !== (value = get())) { var start = input.selectionStart; var end = input.selectionEnd; + var length = input.value.length; // the value is coerced on assignment input.value = value ?? ''; // Restore selection if (end !== null) { - input.selectionStart = start; - input.selectionEnd = Math.min(end, input.value.length); + var new_length = input.value.length; + // If cursor was at end and new input is longer, move cursor to new end + if (start === end && end === length && new_length > length) { + input.selectionStart = new_length; + input.selectionEnd = new_length; + } else { + input.selectionStart = start; + input.selectionEnd = Math.min(end, new_length); + } } } }); From 420468a04167bf30c745476a96942ca7bf7d1850 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:52:11 -0400 Subject: [PATCH 17/34] Version Packages (#16920) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fair-aliens-wait.md | 5 ----- .changeset/rude-frogs-train.md | 5 ----- .changeset/spicy-rabbits-drive.md | 5 ----- .changeset/wicked-goats-begin.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/fair-aliens-wait.md delete mode 100644 .changeset/rude-frogs-train.md delete mode 100644 .changeset/spicy-rabbits-drive.md delete mode 100644 .changeset/wicked-goats-begin.md diff --git a/.changeset/fair-aliens-wait.md b/.changeset/fair-aliens-wait.md deleted file mode 100644 index 66e4cf2bd1..0000000000 --- a/.changeset/fair-aliens-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better input cursor restoration for `bind:value` diff --git a/.changeset/rude-frogs-train.md b/.changeset/rude-frogs-train.md deleted file mode 100644 index 06da5dcc1e..0000000000 --- a/.changeset/rude-frogs-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: track the user's getter of `bind:this` diff --git a/.changeset/spicy-rabbits-drive.md b/.changeset/spicy-rabbits-drive.md deleted file mode 100644 index 01834294e1..0000000000 --- a/.changeset/spicy-rabbits-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: generate correct SSR code for the case where `pending` is an attribute diff --git a/.changeset/wicked-goats-begin.md b/.changeset/wicked-goats-begin.md deleted file mode 100644 index 04a22aa310..0000000000 --- a/.changeset/wicked-goats-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: generate correct code for `each` blocks with async body diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2302acd033..70f549ce29 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.39.12 + +### Patch Changes + +- fix: better input cursor restoration for `bind:value` ([#16925](https://github.com/sveltejs/svelte/pull/16925)) + +- fix: track the user's getter of `bind:this` ([#16916](https://github.com/sveltejs/svelte/pull/16916)) + +- fix: generate correct SSR code for the case where `pending` is an attribute ([#16919](https://github.com/sveltejs/svelte/pull/16919)) + +- fix: generate correct code for `each` blocks with async body ([#16923](https://github.com/sveltejs/svelte/pull/16923)) + ## 5.39.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 822f19f001..a2d5a6e401 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.11", + "version": "5.39.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 0f723c4eb0..e520d1248a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.11'; +export const VERSION = '5.39.12'; export const PUBLIC_VERSION = '5'; From b91f9de129d41c2674ff8bd818eb8bbcc984683e Mon Sep 17 00:00:00 2001 From: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com> Date: Tue, 14 Oct 2025 04:19:55 +0900 Subject: [PATCH 18/34] docs: await no longer need pending (#16900) --- documentation/docs/05-special-elements/01-svelte-boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/05-special-elements/01-svelte-boundary.md b/documentation/docs/05-special-elements/01-svelte-boundary.md index 3e91af5d83..40e8d144e1 100644 --- a/documentation/docs/05-special-elements/01-svelte-boundary.md +++ b/documentation/docs/05-special-elements/01-svelte-boundary.md @@ -24,7 +24,7 @@ For the boundary to do anything, one or more of the following must be provided. ### `pending` -As of Svelte 5.36, boundaries with a `pending` snippet can contain [`await`](await-expressions) expressions. This snippet will be shown when the boundary is first created, and will remain visible until all the `await` expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)): +This snippet will be shown when the boundary is first created, and will remain visible until all the [`await`](await-expressions) expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)): ```svelte From ba782f9fdea1fd028a5e54abcb2c2ab05a67dcc7 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:20:22 -0700 Subject: [PATCH 19/34] docs: link to custom renderer issue in Svelte Native discussion (#16896) --- documentation/docs/07-misc/99-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index 1816998299..86489e87e4 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -99,7 +99,7 @@ However, you can use any router library. A sampling of available routers are hig While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms. -Svelte Native was an option available for Svelte 4, but note that Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native. +Some work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet available. The custom rendering API would support additional mobile frameworks like Lynx JS and Svelte Native. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native. ## Can I tell Svelte not to remove my unused styles? From b05e12fd63a9cf1957ac2ecd27440fc6248ac298 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Oct 2025 15:30:58 -0400 Subject: [PATCH 20/34] fix code block (#16937) Updated code block syntax from Svelte to JavaScript for clarity. --- documentation/docs/02-runes/03-$derived.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 0123868c4e..308693d19c 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -85,7 +85,7 @@ Derived expressions are recalculated when their dependencies change, but you can Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)... -```svelte +```js let items = $state([...]); let index = $state(0); From 23e2bb3b89cf27bd4526c59a03cee86df1bd3eb2 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:22:44 +0200 Subject: [PATCH 21/34] fix: unset context on stale promises (#16935) * fix: unset context on stale promises When a stale promise is rejected in `async_derived`, and the promise eventually resolves, `d.resolve` will be noop and `d.promise.then(handler, ...)` will never run. That in turns means any restored context (via `(await save(..))()`) will never be unset. We have to handle this case and unset the context to prevent errors such as false-positive state mutation errors * fix: unset context on stale promises (slightly different approach) (#16936) * slightly different approach to #16935 * move unset_context call * get rid of logs --------- Co-authored-by: Rich Harris --- .changeset/major-beans-fry.md | 5 +++ .../internal/client/reactivity/deriveds.js | 6 ++-- .../samples/async-resolve-stale/_config.js | 26 ++++++++++++++ .../samples/async-resolve-stale/main.svelte | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 .changeset/major-beans-fry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte diff --git a/.changeset/major-beans-fry.md b/.changeset/major-beans-fry.md new file mode 100644 index 0000000000..8f35683cd6 --- /dev/null +++ b/.changeset/major-beans-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unset context on stale promises diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5d5976a6c1..076a919236 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -126,9 +126,11 @@ export function async_derived(fn, location) { try { // If this code is changed at some point, make sure to still access the then property // of fn() to read any signals it might access, so that we track them as dependencies. - Promise.resolve(fn()).then(d.resolve, d.reject); + // We call `unset_context` to undo any `save` calls that happen inside `fn()` + Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context); } catch (error) { d.reject(error); + unset_context(); } if (DEV) current_async_effect = null; @@ -185,8 +187,6 @@ export function async_derived(fn, location) { boundary.update_pending_count(-1); if (!pending) batch.decrement(); } - - unset_context(); }; d.promise.then(handler, (e) => handler(null, e || 'unknown')); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js new file mode 100644 index 0000000000..bccf12562a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -0,0 +1,26 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + // We gotta wait a bit more in this test because of the macrotasks in App.svelte + function macrotask(t = 3) { + return new Promise((r) => setTimeout(r, t)); + } + + await macrotask(); + assert.htmlEqual(target.innerHTML, ' 1 | '); + + const [input] = target.querySelectorAll('input'); + + input.value = '1'; + input.dispatchEvent(new Event('input', { bubbles: true })); + await macrotask(); + assert.htmlEqual(target.innerHTML, ' 1 | '); + + input.value = '12'; + input.dispatchEvent(new Event('input', { bubbles: true })); + await macrotask(6); + // TODO this is wrong (separate bug), this should be 3 | 12 + assert.htmlEqual(target.innerHTML, ' 5 | 12'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte new file mode 100644 index 0000000000..dc4a157928 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte @@ -0,0 +1,34 @@ + + + + +{count} | {x} From e8330ee7bfff1eef61d5990e99c75dc6e225b5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20R=C3=BCger?= Date: Tue, 14 Oct 2025 15:56:41 +0200 Subject: [PATCH 22/34] fix: svg `radialGradient` `fr` attribute missing in types (#16943) * fix(svg radialGradient): fr attribute missing in types * chore: add changeset --- .changeset/grumpy-towns-stop.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/grumpy-towns-stop.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md new file mode 100644 index 0000000000..2b146818f5 --- /dev/null +++ b/.changeset/grumpy-towns-stop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index b0c2fae2de..17ff100729 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1658,6 +1658,7 @@ export interface SVGAttributes extends AriaAttributes, DO 'font-variant'?: number | string | undefined | null; 'font-weight'?: number | string | undefined | null; format?: number | string | undefined | null; + fr?: number | string | undefined | null; from?: number | string | undefined | null; fx?: number | string | undefined | null; fy?: number | string | undefined | null; From 2a951391dc31326fd5df14bf1bfcf425bd13d5ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:59:09 -0400 Subject: [PATCH 23/34] Version Packages (#16940) * Version Packages * Update packages/svelte/CHANGELOG.md --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/grumpy-towns-stop.md | 5 ----- .changeset/major-beans-fry.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/grumpy-towns-stop.md delete mode 100644 .changeset/major-beans-fry.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md deleted file mode 100644 index 2b146818f5..0000000000 --- a/.changeset/grumpy-towns-stop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/.changeset/major-beans-fry.md b/.changeset/major-beans-fry.md deleted file mode 100644 index 8f35683cd6..0000000000 --- a/.changeset/major-beans-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: unset context on stale promises diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 70f549ce29..b3af39eb4c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.39.13 + +### Patch Changes + +- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943)) + +- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935)) + ## 5.39.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a2d5a6e401..55b44fb2b6 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.12", + "version": "5.39.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e520d1248a..536a2260c9 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.12'; +export const VERSION = '5.39.13'; export const PUBLIC_VERSION = '5'; From a7c958a2a5a89e1d22fa530e2bacd2b0e3ba7ba6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:12:07 -0400 Subject: [PATCH 24/34] chore: simplify `batch.apply()` (#16945) * chore: simplify `batch.apply()` * belt and braces * note to self --- .changeset/pretty-llamas-explode.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 +++++++------------ .../internal/client/reactivity/deriveds.js | 8 ++- .../svelte/src/internal/client/runtime.js | 10 ++- 4 files changed, 41 insertions(+), 49 deletions(-) create mode 100644 .changeset/pretty-llamas-explode.md diff --git a/.changeset/pretty-llamas-explode.md b/.changeset/pretty-llamas-explode.md new file mode 100644 index 0000000000..00109112de --- /dev/null +++ b/.changeset/pretty-llamas-explode.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify `batch.apply()` diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 102d0670b6..0dc149260a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -44,12 +44,12 @@ export let current_batch = null; export let previous_batch = null; /** - * When time travelling, we re-evaluate deriveds based on the temporary - * values of their dependencies rather than their actual values, and cache - * the results in this map rather than on the deriveds themselves - * @type {Map | null} + * When time travelling (i.e. working in one batch, while other batches + * still have ongoing work), we ignore the real values of affected + * signals in favour of their values within the batch + * @type {Map | null} */ -export let batch_deriveds = null; +export let batch_values = null; /** @type {Set<() => void>} */ export let effect_pending_updates = new Set(); @@ -152,7 +152,7 @@ export class Batch { previous_batch = null; - var revert = Batch.apply(this); + this.apply(); for (const root of root_effects) { this.#traverse_effect_tree(root); @@ -161,6 +161,10 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit if (this.#pending === 0) { + // TODO we need this because we commit _then_ flush effects... + // maybe there's a way we can reverse the order? + var previous_batch_sources = batch_values; + this.#commit(); var render_effects = this.#render_effects; @@ -175,6 +179,7 @@ export class Batch { previous_batch = this; current_batch = null; + batch_values = previous_batch_sources; flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -187,7 +192,7 @@ export class Batch { this.#defer_effects(this.#block_effects); } - revert(); + batch_values = null; for (const effect of this.#boundary_async_effects) { update_effect(effect); @@ -274,6 +279,7 @@ export class Batch { } this.current.set(source, source.v); + batch_values?.set(source, source.v); } activate() { @@ -282,6 +288,7 @@ export class Batch { deactivate() { current_batch = null; + batch_values = null; } flush() { @@ -352,14 +359,14 @@ export class Batch { if (queued_root_effects.length > 0) { current_batch = batch; - const revert = Batch.apply(batch); + batch.apply(); for (const root of queued_root_effects) { batch.#traverse_effect_tree(root); } queued_root_effects = []; - revert(); + batch.deactivate(); } } @@ -423,49 +430,23 @@ export class Batch { queue_micro_task(task); } - /** - * @param {Batch} current_batch - */ - static apply(current_batch) { - if (!async_mode_flag || batches.size === 1) { - return noop; - } + apply() { + if (!async_mode_flag || batches.size === 1) return; // if there are multiple batches, we are 'time travelling' — - // we need to undo the changes belonging to any batch - // other than the current one - - /** @type {Map} */ - var current_values = new Map(); - batch_deriveds = new Map(); - - for (const [source, current] of current_batch.current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; - } + // we need to override values with the ones in this batch... + batch_values = new Map(this.current); + // ...and undo changes belonging to other batches for (const batch of batches) { - if (batch === current_batch) continue; + if (batch === this) continue; for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; + if (!batch_values.has(source)) { + batch_values.set(source, previous); } } } - - return () => { - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; - } - } - - batch_deriveds = null; - }; } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 076a919236..bf8733cfe5 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -33,7 +33,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_deriveds, current_batch } from './batch.js'; +import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; @@ -336,6 +336,8 @@ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { + // TODO can we avoid setting `derived.v` when `batch_values !== null`, + // without causing the value to be stale later? derived.v = value; derived.wv = increment_write_version(); } @@ -346,8 +348,8 @@ export function update_derived(derived) { return; } - if (batch_deriveds !== null) { - batch_deriveds.set(derived, derived.v); + if (batch_values !== null) { + batch_values.set(derived, derived.v); } else { var status = (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b8f5f5ffc9..a146659bf6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -42,7 +42,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; +import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -671,8 +671,8 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); - if (batch_deriveds?.has(derived)) { - return batch_deriveds.get(derived); + if (batch_values?.has(derived)) { + return batch_values.get(derived); } if (is_dirty(derived)) { @@ -680,6 +680,10 @@ export function get(signal) { } } + if (batch_values?.has(signal)) { + return batch_values.get(signal); + } + if ((signal.f & ERROR_VALUE) !== 0) { throw signal.v; } From d50701d277e5c57204f1e51314926b0d73b0cbf9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:36:55 -0400 Subject: [PATCH 25/34] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0dc149260a..2edfc1343a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,7 +14,7 @@ import { DERIVED } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; -import { deferred, define_property, noop } from '../../shared/utils.js'; +import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, is_dirty, From 28765f846e956c0da0e41e634de8c1066da9e6d5 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:59:02 +0200 Subject: [PATCH 26/34] fix: don't rerun async effects unnecessarily (#16944) Since #16866, when an async effect runs multiple times, we rebase older batches and rerun those effects. This can have unintended consequences: In a case where an async effect only depends on a single source, and that single source was updated in a later batch, we know that we don't need to / should not rerun the older batch. This PR makes it so: We collect all the sources of older batches that are not part of the current batch that just committed, and then only mark those async effects as dirty which depend on one of those other sources. Fixes the bug I noticed while working on #16935 --- .changeset/wild-mirrors-take.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 7 ++ .../samples/async-resolve-stale/_config.js | 3 +- 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 .changeset/wild-mirrors-take.md diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md new file mode 100644 index 0000000000..faf28e7695 --- /dev/null +++ b/.changeset/wild-mirrors-take.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't rerun async effects unnecessarily diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2edfc1343a..2956e7ed6a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Source, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ import { BLOCK_EFFECT, BRANCH_EFFECT, @@ -342,31 +342,46 @@ export class Batch { continue; } + /** @type {Source[]} */ + const sources = []; + for (const [source, value] of this.current) { if (batch.current.has(source)) { - if (is_earlier) { + if (is_earlier && value !== batch.current.get(source)) { // bring the value up to date batch.current.set(source, value); } else { - // later batch has more recent value, + // same value or later batch has more recent value, // no need to re-run these effects continue; } } - mark_effects(source); + sources.push(source); } - if (queued_root_effects.length > 0) { - current_batch = batch; - batch.apply(); + if (sources.length === 0) { + continue; + } - for (const root of queued_root_effects) { - batch.#traverse_effect_tree(root); + // Re-run async/block effects that depend on distinct values changed in both batches + const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); + if (others.length > 0) { + for (const source of sources) { + mark_effects(source, others); } - queued_root_effects = []; - batch.deactivate(); + if (queued_root_effects.length > 0) { + current_batch = batch; + batch.apply(); + + for (const root of queued_root_effects) { + batch.#traverse_effect_tree(root); + } + + queued_root_effects = []; + batch.deactivate(); + } } } @@ -621,17 +636,19 @@ function flush_queued_effects(effects) { /** * This is similar to `mark_reactions`, but it only marks async/block effects - * so that these can re-run after another batch has been committed + * depending on `value` and at least one of the other `sources`, so that + * these effects can re-run after another batch has been committed * @param {Value} value + * @param {Source[]} sources */ -function mark_effects(value) { +function mark_effects(value, sources) { if (value.reactions !== null) { for (const reaction of value.reactions) { const flags = reaction.f; if ((flags & DERIVED) !== 0) { - mark_effects(/** @type {Derived} */ (reaction)); - } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) { + mark_effects(/** @type {Derived} */ (reaction), sources); + } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) { set_signal_status(reaction, DIRTY); schedule_effect(/** @type {Effect} */ (reaction)); } @@ -639,6 +656,26 @@ function mark_effects(value) { } } +/** + * @param {Reaction} reaction + * @param {Source[]} sources + */ +function depends_on(reaction, sources) { + if (reaction.deps !== null) { + for (const dep of reaction.deps) { + if (sources.includes(dep)) { + return true; + } + + if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) { + return true; + } + } + } + + return false; +} + /** * @param {Effect} signal * @returns {void} diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bf8733cfe5..fa780013e1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -171,6 +171,13 @@ export function async_derived(fn, location) { internal_set(signal, value); + // All prior async derived runs are now stale + for (const [b, d] of deferreds) { + deferreds.delete(b); + if (b === batch) break; + d.reject(STALE_REACTION); + } + if (DEV && location !== undefined) { recent_async_deriveds.add(signal); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js index bccf12562a..50bb414afc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -20,7 +20,6 @@ export default test({ input.value = '12'; input.dispatchEvent(new Event('input', { bubbles: true })); await macrotask(6); - // TODO this is wrong (separate bug), this should be 3 | 12 - assert.htmlEqual(target.innerHTML, ' 5 | 12'); + assert.htmlEqual(target.innerHTML, ' 3 | 12'); } }); From 99711d582263ce3dc0103baecf579e995363db86 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:25:52 +0200 Subject: [PATCH 27/34] fix: ensure map iteration order is correct (#16947) quick follow-up to #16944 Resetting a map entry does not change its position in the map when iterating. We need to make sure that reset makes that batch jump "to the front" for the "reject all stale batches" logic below. Edge case for which I can't come up with a test case but it _is_ a possibility. --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fa780013e1..6aa9a1d9d9 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -144,6 +144,7 @@ export function async_derived(fn, location) { batch.increment(); deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below deferreds.set(batch, d); } } From f3c55e8e6c26bc8c006c84a656c845584abf3eb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 15:48:29 -0400 Subject: [PATCH 28/34] feat: add `createContext` utility for type-safe context (#16948) * feat: add `createContext` utility for type-safe context * regenerate --- .changeset/neat-melons-cheer.md | 5 +++++ .../98-reference/.generated/shared-errors.md | 8 ++++++++ .../svelte/messages/shared-errors/errors.md | 6 ++++++ packages/svelte/src/index-client.js | 8 +++++++- packages/svelte/src/index-server.js | 8 +++++++- .../svelte/src/internal/client/context.js | 20 +++++++++++++++++++ .../svelte/src/internal/server/context.js | 9 +++++++++ packages/svelte/src/internal/shared/errors.js | 16 +++++++++++++++ .../samples/create-context/Child.svelte | 7 +++++++ .../samples/create-context/_config.js | 5 +++++ .../samples/create-context/main.svelte | 16 +++++++++++++++ packages/svelte/types/index.d.ts | 4 ++++ 12 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-melons-cheer.md create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/main.svelte diff --git a/.changeset/neat-melons-cheer.md b/.changeset/neat-melons-cheer.md new file mode 100644 index 0000000000..1107d7ef20 --- /dev/null +++ b/.changeset/neat-melons-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `createContext` utility for type-safe context diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 6c31aaafd0..07e13dea45 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -60,6 +60,14 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +### missing_context + +``` +Context was not set in a parent component +``` + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ### snippet_without_render_tag ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 4b4d332202..e3959034a3 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -52,6 +52,12 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +## missing_context + +> Context was not set in a parent component + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ## snippet_without_render_tag > Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 85eeab7de9..337cbb500b 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -242,7 +242,13 @@ function init_update_callbacks(context) { } export { flushSync } from './internal/client/reactivity/batch.js'; -export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; +export { + createContext, + getContext, + getAllContexts, + hasContext, + setContext +} from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack, settled } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index f193c46894..223ce6a4cd 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -39,6 +39,12 @@ export async function settled() {} export { getAbortSignal } from './internal/server/abort-signal.js'; -export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; +export { + createContext, + getAllContexts, + getContext, + hasContext, + setContext +} from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index cad75546d4..ea63072a37 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -69,6 +69,26 @@ export function set_dev_current_component_function(fn) { dev_current_component_function = fn; } +/** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + + return [ + () => { + if (!hasContext(key)) { + e.missing_context(); + } + + return getContext(key); + }, + (context) => setContext(key, context) + ]; +} + /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index c59b2d260a..1813bfbf78 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -10,6 +10,15 @@ export function set_ssr_context(v) { ssr_context = v; } +/** + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + return [() => getContext(key), (context) => setContext(key, context)]; +} + /** * @template T * @param {any} key diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 6bcc35016a..669cdd96a7 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -51,6 +51,22 @@ export function lifecycle_outside_component(name) { } } +/** + * Context was not set in a parent component + * @returns {never} + */ +export function missing_context() { + if (DEV) { + const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/missing_context`); + } +} + /** * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. * @returns {never} diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte new file mode 100644 index 0000000000..3e39d5043e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte @@ -0,0 +1,7 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js new file mode 100644 index 0000000000..4ae28e68bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

hello

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte new file mode 100644 index 0000000000..8d3c50ba55 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 6dc6629faa..58e3285e4a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,6 +448,10 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * */ + export function createContext(): [() => T, (context: T) => T]; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. From 005895d9940eea9006b1d36f2b0d7260a15a91a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:58:42 -0400 Subject: [PATCH 29/34] Version Packages (#16946) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/neat-melons-cheer.md | 5 ----- .changeset/pretty-llamas-explode.md | 5 ----- .changeset/wild-mirrors-take.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/neat-melons-cheer.md delete mode 100644 .changeset/pretty-llamas-explode.md delete mode 100644 .changeset/wild-mirrors-take.md diff --git a/.changeset/neat-melons-cheer.md b/.changeset/neat-melons-cheer.md deleted file mode 100644 index 1107d7ef20..0000000000 --- a/.changeset/neat-melons-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `createContext` utility for type-safe context diff --git a/.changeset/pretty-llamas-explode.md b/.changeset/pretty-llamas-explode.md deleted file mode 100644 index 00109112de..0000000000 --- a/.changeset/pretty-llamas-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify `batch.apply()` diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md deleted file mode 100644 index faf28e7695..0000000000 --- a/.changeset/wild-mirrors-take.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't rerun async effects unnecessarily diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b3af39eb4c..5d23dddd15 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.40.0 + +### Minor Changes + +- feat: add `createContext` utility for type-safe context ([#16948](https://github.com/sveltejs/svelte/pull/16948)) + +### Patch Changes + +- chore: simplify `batch.apply()` ([#16945](https://github.com/sveltejs/svelte/pull/16945)) + +- fix: don't rerun async effects unnecessarily ([#16944](https://github.com/sveltejs/svelte/pull/16944)) + ## 5.39.13 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 55b44fb2b6..8c049e0f85 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.39.13", + "version": "5.40.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 536a2260c9..2fcca0bd8d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.39.13'; +export const VERSION = '5.40.0'; export const PUBLIC_VERSION = '5'; From 9cdd76e3a3666b6c041fc2eb2c4498db31c795e5 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 14 Oct 2025 14:56:49 -0600 Subject: [PATCH 30/34] chore: Remove annoying sync-async warning (#16949) --- .changeset/eager-cups-argue.md | 5 +++++ .../docs/98-reference/.generated/server-warnings.md | 9 --------- .../svelte/messages/server-warnings/warnings.md | 5 ----- packages/svelte/src/internal/server/renderer.js | 2 -- packages/svelte/src/internal/server/warnings.js | 13 +------------ packages/svelte/src/legacy/legacy-server.js | 1 - 6 files changed, 6 insertions(+), 29 deletions(-) create mode 100644 .changeset/eager-cups-argue.md delete mode 100644 documentation/docs/98-reference/.generated/server-warnings.md delete mode 100644 packages/svelte/messages/server-warnings/warnings.md diff --git a/.changeset/eager-cups-argue.md b/.changeset/eager-cups-argue.md new file mode 100644 index 0000000000..74f03bc1da --- /dev/null +++ b/.changeset/eager-cups-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: Remove sync-in-async warning for server rendering diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md deleted file mode 100644 index 26b3628be9..0000000000 --- a/documentation/docs/98-reference/.generated/server-warnings.md +++ /dev/null @@ -1,9 +0,0 @@ - - -### experimental_async_ssr - -``` -Attempted to use asynchronous rendering without `experimental.async` enabled -``` - -Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously. diff --git a/packages/svelte/messages/server-warnings/warnings.md b/packages/svelte/messages/server-warnings/warnings.md deleted file mode 100644 index 4df89d0176..0000000000 --- a/packages/svelte/messages/server-warnings/warnings.md +++ /dev/null @@ -1,5 +0,0 @@ -## experimental_async_ssr - -> Attempted to use asynchronous rendering without `experimental.async` enabled - -Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously. diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index bbb43a6f3b..602c680c08 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -4,7 +4,6 @@ import { async_mode_flag } from '../flags/index.js'; import { abort } from './abort-signal.js'; import { pop, push, set_ssr_context, ssr_context } from './context.js'; import * as e from './errors.js'; -import * as w from './warnings.js'; import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { attributes } from './index.js'; @@ -361,7 +360,6 @@ export class Renderer { */ (onfulfilled, onrejected) => { if (!async_mode_flag) { - w.experimental_async_ssr(); const result = (sync ??= Renderer.#render(component, options)); const user_result = onfulfilled({ head: result.head, diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js index d8d9cd6d43..d4ee7a86c2 100644 --- a/packages/svelte/src/internal/server/warnings.js +++ b/packages/svelte/src/internal/server/warnings.js @@ -3,15 +3,4 @@ import { DEV } from 'esm-env'; var bold = 'font-weight: bold'; -var normal = 'font-weight: normal'; - -/** - * Attempted to use asynchronous rendering without `experimental.async` enabled - */ -export function experimental_async_ssr() { - if (DEV) { - console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal); - } else { - console.warn(`https://svelte.dev/e/experimental_async_ssr`); - } -} \ No newline at end of file +var normal = 'font-weight: normal'; \ No newline at end of file diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index b7d3e673bc..a50d961751 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -53,7 +53,6 @@ export function asClassComponent(component) { */ value: (onfulfilled, onrejected) => { if (!async_mode_flag) { - w.experimental_async_ssr(); const user_result = onfulfilled({ css: munged.css, head: munged.head, From ec99d7bc4c57e130183f48e10e5b2f37fb173c97 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 19:02:16 -0400 Subject: [PATCH 31/34] fix docs (#16953) --- documentation/docs/02-runes/03-$derived.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 308693d19c..d3e46eb22d 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -86,7 +86,8 @@ Derived expressions are recalculated when their dependencies change, but you can Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)... ```js -let items = $state([...]); +// @errors: 7005 +let items = $state([ /*...*/ ]); let index = $state(0); let selected = $derived(items[index]); From d6d13ced086f5ddb617db2bcd524b2417dd5eb14 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 20:45:29 -0400 Subject: [PATCH 32/34] docs: improve createContext documentation (#16952) * docs: improve createContext documentation * update context page * bump --- documentation/docs/06-runtime/02-context.md | 21 ++++++------------- .../svelte/src/internal/client/context.js | 8 +++++++ .../svelte/src/internal/server/context.js | 1 + packages/svelte/types/index.d.ts | 10 ++++++++- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index f395de421c..0dfb996164 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -83,27 +83,18 @@ Svelte will warn you if you get it wrong. ## Type-safe context -A useful pattern is to wrap the calls to `setContext` and `getContext` inside helper functions that let you preserve type safety: +As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key: -```js -/// file: context.js +```ts +/// file: context.ts // @filename: ambient.d.ts interface User {} -// @filename: index.js +// @filename: index.ts // ---cut--- -import { getContext, setContext } from 'svelte'; - -const key = {}; - -/** @param {User} user */ -export function setUserContext(user) { - setContext(key, user); -} +import { createContext } from 'svelte'; -export function getUserContext() { - return /** @type {User} */ (getContext(key)); -} +export const [getUserContext, setUserContext] = createContext(); ``` ## Replacing global state diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index ea63072a37..751a35321a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -71,8 +71,12 @@ export function set_dev_current_component_function(fn) { /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * + * `get` will throw an error if no parent component called `set`. + * * @template T * @returns {[() => T, (context: T) => T]} + * @since 5.40.0 */ export function createContext() { const key = {}; @@ -93,6 +97,8 @@ export function createContext() { * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. * + * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. + * * @template T * @param {any} key * @returns {T} @@ -110,6 +116,8 @@ export function getContext(key) { * * Like lifecycle functions, this must be called during component initialisation. * + * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. + * * @template T * @param {any} key * @param {T} context diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 1813bfbf78..7779da4c1d 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -13,6 +13,7 @@ export function set_ssr_context(v) { /** * @template T * @returns {[() => T, (context: T) => T]} + * @since 5.40.0 */ export function createContext() { const key = {}; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 58e3285e4a..a9938fe924 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -450,12 +450,18 @@ declare module 'svelte' { type NotFunction = T extends Function ? never : T; /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. - * */ + * + * `get` will throw an error if no parent component called `set`. + * + * @since 5.40.0 + */ export function createContext(): [() => T, (context: T) => T]; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. * + * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. + * * */ export function getContext(key: any): T; /** @@ -465,6 +471,8 @@ declare module 'svelte' { * * Like lifecycle functions, this must be called during component initialisation. * + * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. + * * */ export function setContext(key: any, context: T): T; /** From 470d61221ec3fc2b80ec889853c5d2c9adcf4c50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Oct 2025 09:50:56 -0400 Subject: [PATCH 33/34] chore: tidy up (#16957) --- packages/svelte/src/ambient.d.ts | 6 +++--- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/types/index.d.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index d655fb648a..1f1b0e7b5e 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -100,7 +100,7 @@ declare namespace $state { * you must reassign it. * * Example: - * ```ts + * ```svelte * * - * * ``` @@ -124,7 +124,7 @@ declare namespace $state { * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * * Example: - * ```ts + * ```svelte * * - * * ``` @@ -3222,7 +3222,7 @@ declare namespace $state { * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * * Example: - * ```ts + * ```svelte *