From eea8a18cf2ff8245f0687e8012d5923c5d508d03 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 3 Nov 2025 21:02:09 +0100
Subject: [PATCH 01/88] fix: ensure fork always accesses correct values
(#17098)
Not all batches will flush right after being activated, some will be activated and then `get` is called on a signal. In that case the value was wrong because we did not apply the changes of that batch. By doing `this.apply()` during `activate()` we ensure we do, which fixes (among other things, likely) a forking bug where old values where sneaking in.
Fixes #17079
---
.changeset/eight-news-laugh.md | 5 +++++
.../src/internal/client/reactivity/batch.js | 13 +++++++++----
.../samples/async-fork-if/Child.svelte | 8 ++++++++
.../samples/async-fork-if/_config.js | 12 ++++++++++++
.../samples/async-fork-if/main.svelte | 17 +++++++++++++++++
5 files changed, 51 insertions(+), 4 deletions(-)
create mode 100644 .changeset/eight-news-laugh.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md
new file mode 100644
index 0000000000..e120b19f5e
--- /dev/null
+++ b/.changeset/eight-news-laugh.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure fork always accesses correct values
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 27c90d7708..57aa185a31 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -15,7 +15,8 @@ import {
DERIVED,
BOUNDARY_EFFECT,
EAGER_EFFECT,
- HEAD_EFFECT
+ HEAD_EFFECT,
+ ERROR_VALUE
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
@@ -285,12 +286,16 @@ export class Batch {
this.previous.set(source, value);
}
- this.current.set(source, source.v);
- batch_values?.set(source, source.v);
+ // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
+ if ((source.f & ERROR_VALUE) === 0) {
+ this.current.set(source, source.v);
+ batch_values?.set(source, source.v);
+ }
}
activate() {
current_batch = this;
+ this.apply();
}
deactivate() {
@@ -492,7 +497,7 @@ export class Batch {
}
apply() {
- if (!async_mode_flag || batches.size === 1) return;
+ if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return;
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
new file mode 100644
index 0000000000..6ef7d03eea
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
@@ -0,0 +1,8 @@
+
+
+{x}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
new file mode 100644
index 0000000000..1bc168d9ae
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
@@ -0,0 +1,12 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const btn = target.querySelector('button');
+
+ btn?.click();
+ await new Promise((r) => setTimeout(r, 2));
+ assert.htmlEqual(target.innerHTML, ` universe`);
+ assert.deepEqual(logs, ['universe', 'universe']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
new file mode 100644
index 0000000000..625040ec13
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+{#if x === 'universe'}
+
+{/if}
From 7a2435471c96c43d6f00144cdb5889772703d786 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 3 Nov 2025 20:53:51 -0500
Subject: [PATCH 02/88] Version Packages (#17087)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/eight-news-laugh.md | 5 -----
.changeset/legal-mangos-peel.md | 5 -----
.changeset/sixty-comics-bow.md | 5 -----
packages/svelte/CHANGELOG.md | 10 ++++++++++
packages/svelte/package.json | 2 +-
packages/svelte/src/version.js | 2 +-
6 files changed, 12 insertions(+), 17 deletions(-)
delete mode 100644 .changeset/eight-news-laugh.md
delete mode 100644 .changeset/legal-mangos-peel.md
delete mode 100644 .changeset/sixty-comics-bow.md
diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md
deleted file mode 100644
index e120b19f5e..0000000000
--- a/.changeset/eight-news-laugh.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: ensure fork always accesses correct values
diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md
deleted file mode 100644
index bddad21bff..0000000000
--- a/.changeset/legal-mangos-peel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: change title only after any pending work has completed
diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md
deleted file mode 100644
index 2463e52430..0000000000
--- a/.changeset/sixty-comics-bow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: preserve symbols when creating derived rest properties
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index bcf17fe45e..bc2f815908 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,15 @@
# svelte
+## 5.43.3
+
+### Patch Changes
+
+- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
+
+- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
+
+- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
+
## 5.43.2
### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index f7a1cca616..f178444593 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.43.2",
+ "version": "5.43.3",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 0a28702778..5ad40ddee6 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.43.2';
+export const VERSION = '5.43.3';
export const PUBLIC_VERSION = '5';
From 46e9d2d357d724479fbf6db6c0d9ebb22bab9796 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 6 Nov 2025 05:47:39 -0500
Subject: [PATCH 03/88] chore: remove `UNOWNED` flag (#17105)
Fixes #17024
Fixes #17049 (comment) (and therefore everything that was still buggy in that issue I think)
* chore: remove unowned check when calling `e.effect_in_unowned_derived`
* WIP
* all non-unit tests passing
* tidy
* WIP
* WIP
* WIP
* note to self
* fix
* fix
* hmm maybe not
* try this
* simplify
* remove skip_reaction
* docs
* add changeset, in case this results in changed behaviour
* Update packages/svelte/src/internal/client/reactivity/effects.js
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
* fix #17024
* fix comment
* revert
* fix
* dry
* changeset
* fix WAS_MARKED logic
* failing test (that uncovered other unrelated bug) + fix
* fix: delete from batch_values on updates (#17115)
* fix: delete from batch_values on updates
This fixes a bug where a derived would still show its old value even after it was indirectly updated again within the same batch. This can for example happen by reading a derived on an effect, then writing to a source in that same effect that makes the derived update, and then read the derived value in a sibling effect - it still shows the old value without the fix.
The fix is to _delete_ the value from batch_values, as it's now the newest value across all batches. In order to not prevent breakage on other batches we have to leave the status of deriveds as-is, i.e. within is_dirty and update_derived we cannot update its status. That might be a bit more inefficient as you now have to traverse the graph for each `get` of that derived (it's a bit like they are all disconnected) but we can always optimize that later if need be.
Another advantage of this fix is that we can get rid of duplicate logic we had to add about unmarking and reconnecting deriveds, because that logic was only needed for the case where deriveds are read after they are updated, which now no longer hits that if-branch
* keep derived cache, but clear it in mark_reactions (#17116)
---------
Co-authored-by: Rich Harris
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen
---
.changeset/four-paths-cheer.md | 5 +
.changeset/whole-webs-stick.md | 5 +
.../svelte/src/internal/client/constants.js | 9 +-
.../internal/client/reactivity/deriveds.js | 23 ++--
.../src/internal/client/reactivity/effects.js | 14 +-
.../src/internal/client/reactivity/sources.js | 21 ++-
.../svelte/src/internal/client/runtime.js | 130 ++++++------------
.../Component.svelte | 27 ++++
.../_config.js | 16 +++
.../main.svelte | 19 +++
.../async-derived-unowned/Component.svelte | 6 +
.../samples/async-derived-unowned/_config.js | 30 ++++
.../samples/async-derived-unowned/main.svelte | 19 +++
13 files changed, 212 insertions(+), 112 deletions(-)
create mode 100644 .changeset/four-paths-cheer.md
create mode 100644 .changeset/whole-webs-stick.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte
diff --git a/.changeset/four-paths-cheer.md b/.changeset/four-paths-cheer.md
new file mode 100644
index 0000000000..54a697a8a4
--- /dev/null
+++ b/.changeset/four-paths-cheer.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: simplify connection/disconnection logic
diff --git a/.changeset/whole-webs-stick.md b/.changeset/whole-webs-stick.md
new file mode 100644
index 0000000000..fe8b614a01
--- /dev/null
+++ b/.changeset/whole-webs-stick.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: reconnect deriveds to effect tree when time-travelling
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index c2f7861b78..b39afef516 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
+/**
+ * Indicates that a reaction is connected to an effect root — either it is an effect,
+ * or it is a derived that is depended on by at least one effect. If a derived has
+ * no dependents, we can disconnect it from the graph, allowing it to either be
+ * GC'd or reconnected later if an effect comes to depend on it again
+ */
+export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
@@ -26,8 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
// Flags exclusive to deriveds
-export const UNOWNED = 1 << 8;
-export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 1eb640ad26..7e6f3c6f60 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -9,15 +9,14 @@ import {
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
- UNOWNED,
ASYNC,
- WAS_MARKED
+ WAS_MARKED,
+ CONNECTED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
- skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
@@ -27,7 +26,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
-import { async_effect, destroy_effect, teardown } from './effects.js';
+import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -61,9 +60,7 @@ export function derived(fn) {
? /** @type {Derived} */ (active_reaction)
: null;
- if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
- flags |= UNOWNED;
- } else {
+ if (active_effect !== null) {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_PRESERVED;
@@ -368,12 +365,16 @@ export function update_derived(derived) {
return;
}
+ // During time traveling we don't want to reset the status so that
+ // traversal of the graph in the other batches still happens
if (batch_values !== null) {
- batch_values.set(derived, derived.v);
+ // only cache the value if we're in a tracking context, otherwise we won't
+ // clear the cache in `mark_reactions` when dependencies are updated
+ if (effect_tracking()) {
+ batch_values.set(derived, derived.v);
+ }
} else {
- var status =
- (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
-
+ var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}
}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 8c4b84438c..5d7c0ef871 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -25,7 +25,6 @@ import {
ROOT_EFFECT,
EFFECT_TRANSPARENT,
DERIVED,
- UNOWNED,
CLEAN,
EAGER_EFFECT,
HEAD_EFFECT,
@@ -33,7 +32,8 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
USER_EFFECT,
- ASYNC
+ ASYNC,
+ CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@@ -48,11 +48,11 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js';
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
*/
export function validate_effect(rune) {
- if (active_effect === null && active_reaction === null) {
- e.effect_orphan(rune);
- }
+ if (active_effect === null) {
+ if (active_reaction === null) {
+ e.effect_orphan(rune);
+ }
- if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
e.effect_in_unowned_derived();
}
@@ -103,7 +103,7 @@ function create_effect(type, fn, sync, push = true) {
deps: null,
nodes_start: null,
nodes_end: null,
- f: type | DIRTY,
+ f: type | DIRTY | CONNECTED,
first: null,
fn,
last: null,
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index b480d4155a..8ae406b57b 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -23,18 +23,18 @@ import {
DIRTY,
BRANCH_EFFECT,
EAGER_EFFECT,
- UNOWNED,
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
- WAS_MARKED
+ WAS_MARKED,
+ CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
-import { Batch, eager_block_effects, schedule_effect } from './batch.js';
+import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@@ -211,7 +211,8 @@ export function internal_set(source, value) {
if ((source.f & DIRTY) !== 0) {
execute_derived(/** @type {Derived} */ (source));
}
- set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
+
+ set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY);
}
source.wv = increment_write_version();
@@ -333,9 +334,17 @@ function mark_reactions(signal, status) {
}
if ((flags & DERIVED) !== 0) {
+ var derived = /** @type {Derived} */ (reaction);
+
+ batch_values?.delete(derived);
+
if ((flags & WAS_MARKED) === 0) {
- reaction.f |= WAS_MARKED;
- mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
+ // Only connected deriveds can be reliably unmarked right away
+ if (flags & CONNECTED) {
+ reaction.f |= WAS_MARKED;
+ }
+
+ mark_reactions(derived, MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 76531d3320..258f6962fa 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -4,6 +4,7 @@ import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'
import {
destroy_block_effect_children,
destroy_effect_children,
+ effect_tracking,
execute_effect_teardown
} from './reactivity/effects.js';
import {
@@ -11,13 +12,12 @@ import {
MAYBE_DIRTY,
CLEAN,
DERIVED,
- UNOWNED,
DESTROYED,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
- DISCONNECTED,
+ CONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
@@ -137,10 +137,6 @@ export function set_update_version(value) {
update_version = value;
}
-// If we are working with a get() chain that has no active container,
-// to prevent memory leaks, we skip adding the reaction.
-export let skip_reaction = false;
-
export function increment_write_version() {
return ++write_version;
}
@@ -158,55 +154,18 @@ export function is_dirty(reaction) {
return true;
}
+ if (flags & DERIVED) {
+ reaction.f &= ~WAS_MARKED;
+ }
+
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = reaction.deps;
- var is_unowned = (flags & UNOWNED) !== 0;
-
- if (flags & DERIVED) {
- reaction.f &= ~WAS_MARKED;
- }
if (dependencies !== null) {
- var i;
- var dependency;
- var is_disconnected = (flags & DISCONNECTED) !== 0;
- var is_unowned_connected = is_unowned && active_effect !== null && !skip_reaction;
var length = dependencies.length;
- // If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
- // then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed
- // (which can happen if the derived is read by an async derived)
- if (
- (is_disconnected || is_unowned_connected) &&
- (active_effect === null || (active_effect.f & DESTROYED) === 0)
- ) {
- var derived = /** @type {Derived} */ (reaction);
- var parent = derived.parent;
-
- for (i = 0; i < length; i++) {
- dependency = dependencies[i];
-
- // We always re-add all reactions (even duplicates) if the derived was
- // previously disconnected, however we don't if it was unowned as we
- // de-duplicate dependencies in that case
- if (is_disconnected || !dependency?.reactions?.includes(derived)) {
- (dependency.reactions ??= []).push(derived);
- }
- }
-
- if (is_disconnected) {
- derived.f ^= DISCONNECTED;
- }
- // If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
- // and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
- // flag
- if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
- derived.f ^= UNOWNED;
- }
- }
-
- for (i = 0; i < length; i++) {
- dependency = dependencies[i];
+ for (var i = 0; i < length; i++) {
+ var dependency = dependencies[i];
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
@@ -218,9 +177,12 @@ export function is_dirty(reaction) {
}
}
- // Unowned signals should never be marked as clean unless they
- // are used within an active_effect without skip_reaction
- if (!is_unowned || (active_effect !== null && !skip_reaction)) {
+ if (
+ (flags & CONNECTED) !== 0 &&
+ // During time traveling we don't want to reset the status so that
+ // traversal of the graph in the other batches still happens
+ batch_values === null
+ ) {
set_signal_status(reaction, CLEAN);
}
}
@@ -263,7 +225,6 @@ export function update_reaction(reaction) {
var previous_skipped_deps = skipped_deps;
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
- var previous_skip_reaction = skip_reaction;
var previous_sources = current_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
@@ -274,8 +235,6 @@ export function update_reaction(reaction) {
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
- skip_reaction =
- (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_sources = null;
@@ -311,12 +270,7 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
- if (
- !skip_reaction ||
- // Deriveds that already have reactions can cleanup, so we still add them as reactions
- ((flags & DERIVED) !== 0 &&
- /** @type {import('#client').Derived} */ (reaction).reactions !== null)
- ) {
+ if (is_updating_effect && effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@@ -373,7 +327,6 @@ export function update_reaction(reaction) {
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
- skip_reaction = previous_skip_reaction;
current_sources = previous_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
@@ -415,9 +368,10 @@ function remove_reaction(signal, dependency) {
) {
set_signal_status(dependency, MAYBE_DIRTY);
// If we are working with a derived that is owned by an effect, then mark it as being
- // disconnected.
- if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
- dependency.f ^= DISCONNECTED;
+ // disconnected and remove the mark flag, as it cannot be reliably removed otherwise
+ if ((dependency.f & CONNECTED) !== 0) {
+ dependency.f ^= CONNECTED;
+ dependency.f &= ~WAS_MARKED;
}
// Disconnect any reactions owned by this reaction
destroy_derived_effects(/** @type {Derived} **/ (dependency));
@@ -564,10 +518,7 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
- } else if (!skip_reaction || !new_deps.includes(signal)) {
- // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
- // an unowned derived because skip_reaction is true, then we need to ensure that
- // we don't have duplicates
+ } else if (!new_deps.includes(signal)) {
new_deps.push(signal);
}
}
@@ -585,20 +536,6 @@ export function get(signal) {
}
}
}
- } else if (
- is_derived &&
- /** @type {Derived} */ (signal).deps === null &&
- /** @type {Derived} */ (signal).effects === null
- ) {
- var derived = /** @type {Derived} */ (signal);
- var parent = derived.parent;
-
- if (parent !== null && (parent.f & UNOWNED) === 0) {
- // If the derived is owned by another derived then mark it as unowned
- // as the derived value might have been referenced in a different context
- // since and thus its parent might not be its true owner anymore
- derived.f ^= UNOWNED;
- }
}
if (DEV) {
@@ -657,7 +594,7 @@ export function get(signal) {
}
if (is_derived) {
- derived = /** @type {Derived} */ (signal);
+ var derived = /** @type {Derived} */ (signal);
var value = derived.v;
@@ -684,9 +621,11 @@ export function get(signal) {
if (is_dirty(derived)) {
update_derived(derived);
}
- }
- if (batch_values?.has(signal)) {
+ if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
+ reconnect(derived);
+ }
+ } else if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
@@ -697,6 +636,25 @@ export function get(signal) {
return signal.v;
}
+/**
+ * (Re)connect a disconnected derived, so that it is notified
+ * of changes in `mark_reactions`
+ * @param {Derived} derived
+ */
+function reconnect(derived) {
+ if (derived.deps === null) return;
+
+ derived.f ^= CONNECTED;
+
+ for (const dep of derived.deps) {
+ (dep.reactions ??= []).push(derived);
+
+ if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
+ reconnect(/** @type {Derived} */ (dep));
+ }
+ }
+}
+
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte
new file mode 100644
index 0000000000..200778dc5b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte
@@ -0,0 +1,27 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js
new file mode 100644
index 0000000000..15bb42074f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js
@@ -0,0 +1,16 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const button = target.querySelector('button');
+
+ button?.click();
+ await tick();
+ assert.deepEqual(logs, [5]);
+
+ button?.click();
+ await tick();
+ assert.deepEqual(logs, [5, 7]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte
new file mode 100644
index 0000000000..bd82e35a3b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {await new Promise((r) => {
+ // long enough for the test to do all its other stuff while this is pending
+ setTimeout(r, 10);
+ })}
+ {#snippet pending()}{/snippet}
+
+
+
+
+{#if count > 0}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte
new file mode 100644
index 0000000000..f7d138a3ed
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte
@@ -0,0 +1,6 @@
+
+
+
From ea8838e96f063d274c989523e47f430bd9af9886 Mon Sep 17 00:00:00 2001
From: Paolo Ricciuti
Date: Mon, 24 Nov 2025 22:41:09 +0100
Subject: [PATCH 36/88] fix: store forked derived values (#17212)
We have to take non-tracking contexts into account, especially while in the original `fork(() => ...)` context.
Closes #17206
---------
Co-authored-by: Simon Holthausen
---
.changeset/strong-berries-fry.md | 5 +++++
.../src/internal/client/reactivity/batch.js | 3 +++
.../src/internal/client/reactivity/deriveds.js | 2 +-
packages/svelte/src/internal/client/runtime.js | 13 +++++++++++--
.../fork-derived-value-immediate/_config.js | 13 +++++++++++++
.../fork-derived-value-immediate/main.svelte | 15 +++++++++++++++
6 files changed, 48 insertions(+), 3 deletions(-)
create mode 100644 .changeset/strong-berries-fry.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte
diff --git a/.changeset/strong-berries-fry.md b/.changeset/strong-berries-fry.md
new file mode 100644
index 0000000000..60dbb290a8
--- /dev/null
+++ b/.changeset/strong-berries-fry.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: store forked derived values
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index b99af84764..ee28556a27 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -959,12 +959,15 @@ export function fork(fn) {
var batch = Batch.ensure();
batch.is_fork = true;
+ batch_values = new Map();
var committed = false;
var settled = batch.settled();
flushSync(fn);
+ batch_values = null;
+
// revert state changes
for (var [source, value] of batch.previous) {
source.v = value;
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 39e02be764..3bf38bf0b2 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -378,7 +378,7 @@ export function update_derived(derived) {
if (batch_values !== null) {
// only cache the value if we're in a tracking context, otherwise we won't
// clear the cache in `mark_reactions` when dependencies are updated
- if (effect_tracking()) {
+ if (effect_tracking() || current_batch?.is_fork) {
batch_values.set(derived, value);
}
} else {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index cb0fb74306..4e82950782 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -44,7 +44,13 @@ import {
set_dev_stack
} from './context.js';
import * as w from './warnings.js';
-import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js';
+import {
+ Batch,
+ batch_values,
+ current_batch,
+ 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';
@@ -612,7 +618,10 @@ export function get(signal) {
return value;
}
- } else if (is_derived && !batch_values?.has(signal)) {
+ } else if (
+ is_derived &&
+ (!batch_values?.has(signal) || (current_batch?.is_fork && !effect_tracking()))
+ ) {
derived = /** @type {Derived} */ (signal);
if (is_dirty(derived)) {
diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js
new file mode 100644
index 0000000000..4f7ff673d6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ skip_no_async: true,
+ async test({ assert, target, logs }) {
+ const fork = target.querySelector('button');
+
+ fork?.click();
+ flushSync();
+ assert.deepEqual(logs, [1, 2]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte
new file mode 100644
index 0000000000..2adb83b735
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte
@@ -0,0 +1,15 @@
+
+
+
From 84b261886b9747fc88df818eaf3ac70b802b6eae Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 24 Nov 2025 22:46:34 +0100
Subject: [PATCH 37/88] chore: lift "flushSync cannot be called in effects"
restriction (#17139)
Since async-await was introduced into the code base a lot has changed. This lifts the restriction.
Closes #17131 (though I still wonder why Skeleton does that)
---
.changeset/heavy-lions-tap.md | 5 +++++
.../98-reference/.generated/client-errors.md | 10 ----------
packages/svelte/messages/client-errors/errors.md | 8 --------
packages/svelte/src/internal/client/errors.js | 16 ----------------
.../src/internal/client/reactivity/batch.js | 5 -----
.../flush-sync-inside-attachment/_config.js | 6 +-----
6 files changed, 6 insertions(+), 44 deletions(-)
create mode 100644 .changeset/heavy-lions-tap.md
diff --git a/.changeset/heavy-lions-tap.md b/.changeset/heavy-lions-tap.md
new file mode 100644
index 0000000000..5cf97fda77
--- /dev/null
+++ b/.changeset/heavy-lions-tap.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: lift "flushSync cannot be called in effects" restriction
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 3f1cb8f76b..fa8533928a 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -136,16 +136,6 @@ Often when encountering this issue, the value in question shouldn't be state (fo
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
```
-### flush_sync_in_effect
-
-```
-Cannot use `flushSync` inside an effect
-```
-
-The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
-
-This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
-
### fork_discarded
```
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index ae7d811b2e..b75dc9cc6a 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -104,14 +104,6 @@ Often when encountering this issue, the value in question shouldn't be state (fo
> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
-## flush_sync_in_effect
-
-> Cannot use `flushSync` inside an effect
-
-The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
-
-This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
-
## fork_discarded
> Cannot commit a fork that was already discarded
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 8a5fde4f3b..c132382569 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -245,22 +245,6 @@ export function experimental_async_fork() {
}
}
-/**
- * Cannot use `flushSync` inside an effect
- * @returns {never}
- */
-export function flush_sync_in_effect() {
- if (DEV) {
- const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
-
- error.name = 'Svelte error';
-
- throw error;
- } else {
- throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
- }
-}
-
/**
* Cannot commit a fork that was already discarded
* @returns {never}
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index ee28556a27..c6b182790e 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -563,11 +563,6 @@ export class Batch {
* @returns {T}
*/
export function flushSync(fn) {
- if (async_mode_flag && active_effect !== null) {
- // We disallow this because it creates super-hard to reason about stack trace and because it's generally a bad idea
- e.flush_sync_in_effect();
- }
-
var was_flushing_sync = is_flushing_sync;
is_flushing_sync = true;
diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js
index ec8858b2c6..b34a90e901 100644
--- a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js
@@ -1,12 +1,8 @@
-import { async_mode } from '../../../helpers';
import { test } from '../../test';
export default test({
- // In legacy mode this succeeds and logs 'hello'
- // In async mode this throws an error because flushSync is called inside an effect
async test({ assert, target, logs }) {
assert.htmlEqual(target.innerHTML, `
+```
+
+That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often -- it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
+
+To fix the example above:
+
+```svelte
+
+
+
{user.name}
+```
+
+This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
+
+```ts
+import { hydratable } from 'svelte';
+const rand = hydratable('random', () => Math.random());
+```
+
+If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
+
+## Serialization
+
+All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
+
+```svelte
+
+
+{await promises.one}
+{await promises.two}
+```
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index fa8533928a..8601a728a7 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -130,12 +130,16 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
-### experimental_async_fork
+### flush_sync_in_effect
```
-Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
+Cannot use `flushSync` inside an effect
```
+The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
+
+This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
+
### fork_discarded
```
@@ -154,6 +158,25 @@ Cannot create a fork inside an effect or when state changes are pending
`getAbortSignal()` can only be called inside an effect or derived
```
+### hydratable_missing_but_required
+
+```
+Expected to find a hydratable with key `%key%` during hydration, but did not.
+```
+
+This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
+
+```svelte
+
+```
+
### hydration_failed
```
diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md
index c95ace2229..4deb338521 100644
--- a/documentation/docs/98-reference/.generated/client-warnings.md
+++ b/documentation/docs/98-reference/.generated/client-warnings.md
@@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
%handler% should be a function. Did you mean to %suggestion%?
```
+### hydratable_missing_but_expected
+
+```
+Expected to find a hydratable with key `%key%` during hydration, but did not.
+```
+
+This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
+
+```svelte
+
+```
+
### hydration_attribute_changed
```
diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md
index 6263032212..4d05e04207 100644
--- a/documentation/docs/98-reference/.generated/server-errors.md
+++ b/documentation/docs/98-reference/.generated/server-errors.md
@@ -1,5 +1,13 @@
+### async_local_storage_unavailable
+
+```
+The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
+```
+
+Some platforms require configuration flags to enable this API. Consult your platform's documentation.
+
### await_invalid
```
@@ -14,6 +22,39 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
The `html` property of server render results has been deprecated. Use `body` instead.
```
+### hydratable_clobbering
+
+```
+Attempted to set `hydratable` with key `%key%` twice with different values.
+
+%stack%
+```
+
+This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
+- Ensure all invocations with the same key result in the same value
+- Update the keys to make both instances unique
+
+```svelte
+
+```
+
+### hydratable_serialization_failed
+
+```
+Failed to serialize `hydratable` data for key `%key%`.
+
+`hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
+
+Cause:
+%stack%
+```
+
### lifecycle_function_unavailable
```
@@ -21,3 +62,11 @@ The `html` property of server render results has been deprecated. Use `body` ins
```
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
+
+### server_context_required
+
+```
+Could not resolve `render` context.
+```
+
+Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.
diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md
new file mode 100644
index 0000000000..c4a7fbefef
--- /dev/null
+++ b/documentation/docs/98-reference/.generated/server-warnings.md
@@ -0,0 +1,34 @@
+
+
+### unresolved_hydratable
+
+```
+A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
+
+The `hydratable` was initialized in:
+%stack%
+```
+
+The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
+the result inside a `svelte:boundary` with a `pending` snippet:
+
+```svelte
+
+
+
+
{(await user).name}
+
+ {#snippet pending()}
+
Loading...
+ {/snippet}
+
+```
+
+Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
+
+Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.
diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md
index 07e13dea45..136b3f4957 100644
--- a/documentation/docs/98-reference/.generated/shared-errors.md
+++ b/documentation/docs/98-reference/.generated/shared-errors.md
@@ -1,5 +1,11 @@
+### experimental_async_required
+
+```
+Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
+```
+
### invalid_default_snippet
```
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index b75dc9cc6a..bedf6db0a5 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -100,9 +100,13 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
-## experimental_async_fork
+## flush_sync_in_effect
-> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
+> Cannot use `flushSync` inside an effect
+
+The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
+
+This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## fork_discarded
@@ -116,6 +120,23 @@ Often when encountering this issue, the value in question shouldn't be state (fo
> `getAbortSignal()` can only be called inside an effect or derived
+## hydratable_missing_but_required
+
+> Expected to find a hydratable with key `%key%` during hydration, but did not.
+
+This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
+
+```svelte
+
+```
+
## hydration_failed
> Failed to hydrate the application
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index 9763c8df1a..b51fc6b53c 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -124,6 +124,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %handler% should be a function. Did you mean to %suggestion%?
+## hydratable_missing_but_expected
+
+> Expected to find a hydratable with key `%key%` during hydration, but did not.
+
+This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
+
+```svelte
+
+```
+
## hydration_attribute_changed
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
diff --git a/packages/svelte/messages/server-errors/errors.md b/packages/svelte/messages/server-errors/errors.md
index 49d2a310f6..ac2ecc0a40 100644
--- a/packages/svelte/messages/server-errors/errors.md
+++ b/packages/svelte/messages/server-errors/errors.md
@@ -1,3 +1,9 @@
+## async_local_storage_unavailable
+
+> The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
+
+Some platforms require configuration flags to enable this API. Consult your platform's documentation.
+
## await_invalid
> Encountered asynchronous work while rendering synchronously.
@@ -8,8 +14,43 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
> The `html` property of server render results has been deprecated. Use `body` instead.
+## hydratable_clobbering
+
+> Attempted to set `hydratable` with key `%key%` twice with different values.
+>
+> %stack%
+
+This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
+- Ensure all invocations with the same key result in the same value
+- Update the keys to make both instances unique
+
+```svelte
+
+```
+
+## hydratable_serialization_failed
+
+> Failed to serialize `hydratable` data for key `%key%`.
+>
+> `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
+>
+> Cause:
+> %stack%
+
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
+
+## server_context_required
+
+> Could not resolve `render` context.
+
+Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.
diff --git a/packages/svelte/messages/server-warnings/warnings.md b/packages/svelte/messages/server-warnings/warnings.md
new file mode 100644
index 0000000000..89e1c9d718
--- /dev/null
+++ b/packages/svelte/messages/server-warnings/warnings.md
@@ -0,0 +1,30 @@
+## unresolved_hydratable
+
+> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
+>
+> The `hydratable` was initialized in:
+> %stack%
+
+The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
+the result inside a `svelte:boundary` with a `pending` snippet:
+
+```svelte
+
+
+
+
{(await user).name}
+
+ {#snippet pending()}
+
Loading...
+ {/snippet}
+
+```
+
+Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
+
+Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.
diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md
index e3959034a3..bf053283e4 100644
--- a/packages/svelte/messages/shared-errors/errors.md
+++ b/packages/svelte/messages/shared-errors/errors.md
@@ -1,3 +1,7 @@
+## experimental_async_required
+
+> Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
+
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index fa636a5355..264c672828 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -174,6 +174,7 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
+ "devalue": "^5.5.0",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 4fcfff980d..0eb1b80315 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -249,6 +249,7 @@ export {
hasContext,
setContext
} from './internal/client/context.js';
+export { hydratable } from './internal/client/hydratable.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 61b0d98c06..9fb810fd9e 100644
--- a/packages/svelte/src/index-server.js
+++ b/packages/svelte/src/index-server.js
@@ -51,4 +51,6 @@ export {
setContext
} from './internal/server/context.js';
+export { hydratable } from './internal/server/hydratable.js';
+
export { createRawSnippet } from './internal/server/blocks/snippet.js';
diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js
index 34ba508984..1ed255b4dd 100644
--- a/packages/svelte/src/internal/client/dev/inspect.js
+++ b/packages/svelte/src/internal/client/dev/inspect.js
@@ -2,7 +2,7 @@ import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { eager_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
-import { get_stack } from './tracing.js';
+import { get_error } from '../../shared/dev.js';
/**
* @param {() => any[]} get_value
@@ -33,7 +33,7 @@ export function inspect(get_value, inspector, show_stack = false) {
inspector(...snap);
if (!initial) {
- const stack = get_stack('$inspect(...)');
+ const stack = get_error('$inspect(...)');
// eslint-disable-next-line no-console
if (stack) {
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 183da73447..c6edfde933 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -1,7 +1,6 @@
/** @import { Derived, Reaction, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
-import { define_property } from '../../shared/utils.js';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, untrack } from '../runtime.js';
@@ -131,62 +130,6 @@ export function trace(label, fn) {
}
}
-/**
- * @param {string} label
- * @returns {Error & { stack: string } | null}
- */
-export function get_stack(label) {
- // @ts-ignore stackTraceLimit doesn't exist everywhere
- const limit = Error.stackTraceLimit;
-
- // @ts-ignore
- Error.stackTraceLimit = Infinity;
- let error = Error();
-
- // @ts-ignore
- Error.stackTraceLimit = limit;
-
- const stack = error.stack;
-
- if (!stack) return null;
-
- const lines = stack.split('\n');
- const new_lines = ['\n'];
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const posixified = line.replaceAll('\\', '/');
-
- if (line === 'Error') {
- continue;
- }
-
- if (line.includes('validate_each_keys')) {
- return null;
- }
-
- if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
- continue;
- }
-
- new_lines.push(line);
- }
-
- if (new_lines.length === 1) {
- return null;
- }
-
- define_property(error, 'stack', {
- value: new_lines.join('\n')
- });
-
- define_property(error, 'name', {
- value: label
- });
-
- return /** @type {Error & { stack: string }} */ (error);
-}
-
/**
* @param {Value} source
* @param {string} label
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index c132382569..34f1d85540 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -230,18 +230,18 @@ export function effect_update_depth_exceeded() {
}
/**
- * Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
+ * Cannot use `flushSync` inside an effect
* @returns {never}
*/
-export function experimental_async_fork() {
+export function flush_sync_in_effect() {
if (DEV) {
- const error = new Error(`experimental_async_fork\nCannot use \`fork(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_fork`);
+ const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`);
error.name = 'Svelte error';
throw error;
} else {
- throw new Error(`https://svelte.dev/e/experimental_async_fork`);
+ throw new Error(`https://svelte.dev/e/flush_sync_in_effect`);
}
}
@@ -293,6 +293,23 @@ export function get_abort_signal_outside_reaction() {
}
}
+/**
+ * Expected to find a hydratable with key `%key%` during hydration, but did not.
+ * @param {string} key
+ * @returns {never}
+ */
+export function hydratable_missing_but_required(key) {
+ if (DEV) {
+ const error = new Error(`hydratable_missing_but_required\nExpected to find a hydratable with key \`${key}\` during hydration, but did not.\nhttps://svelte.dev/e/hydratable_missing_but_required`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/hydratable_missing_but_required`);
+ }
+}
+
/**
* Failed to hydrate the application
* @returns {never}
diff --git a/packages/svelte/src/internal/client/hydratable.js b/packages/svelte/src/internal/client/hydratable.js
new file mode 100644
index 0000000000..601b860a48
--- /dev/null
+++ b/packages/svelte/src/internal/client/hydratable.js
@@ -0,0 +1,33 @@
+import { async_mode_flag } from '../flags/index.js';
+import { hydrating } from './dom/hydration.js';
+import * as w from './warnings.js';
+import * as e from './errors.js';
+import { DEV } from 'esm-env';
+
+/**
+ * @template T
+ * @param {string} key
+ * @param {() => T} fn
+ * @returns {T}
+ */
+export function hydratable(key, fn) {
+ if (!async_mode_flag) {
+ e.experimental_async_required('hydratable');
+ }
+
+ if (hydrating) {
+ const store = window.__svelte?.h;
+
+ if (store?.has(key)) {
+ return /** @type {T} */ (store.get(key));
+ }
+
+ if (DEV) {
+ e.hydratable_missing_but_required(key);
+ } else {
+ w.hydratable_missing_but_expected(key);
+ }
+ }
+
+ return fn();
+}
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 5b028d8d09..c8802e2672 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -25,7 +25,8 @@ import {
import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
-import { get_stack, tag } from './dev/tracing.js';
+import { tag } from './dev/tracing.js';
+import { get_error } from '../shared/dev.js';
import { tracing_mode_flag } from '../flags/index.js';
// TODO move all regexes into shared module?
@@ -53,7 +54,7 @@ export function proxy(value) {
var is_proxied_array = is_array(value);
var version = source(0);
- var stack = DEV && tracing_mode_flag ? get_stack('created at') : null;
+ var stack = DEV && tracing_mode_flag ? get_error('created at') : null;
var parent_version = update_version;
/**
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index c6b182790e..3308711ed3 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -945,7 +945,7 @@ export function eager(fn) {
*/
export function fork(fn) {
if (!async_mode_flag) {
- e.experimental_async_fork();
+ e.experimental_async_required('fork');
}
if (current_batch !== null) {
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 3bf38bf0b2..b2d5d59ae1 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -29,7 +29,7 @@ import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
-import { get_stack } from '../dev/tracing.js';
+import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
@@ -84,7 +84,7 @@ export function derived(fn) {
};
if (DEV && tracing_mode_flag) {
- signal.created = get_stack('created at');
+ signal.created = get_error('created at');
}
return signal;
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 4f2adff9de..853b255dd5 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -34,7 +34,8 @@ import {
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
-import { get_stack, tag_proxy } from '../dev/tracing.js';
+import { tag_proxy } from '../dev/tracing.js';
+import { get_error } from '../../shared/dev.js';
import { component_context, is_runes } from '../context.js';
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
@@ -78,7 +79,7 @@ export function source(v, stack) {
};
if (DEV && tracing_mode_flag) {
- signal.created = stack ?? get_stack('created at');
+ signal.created = stack ?? get_error('created at');
signal.updated = null;
signal.set_during_effect = false;
signal.trace = null;
@@ -196,7 +197,7 @@ export function internal_set(source, value) {
source.updated.set('', { error: /** @type {any} */ (null), count });
if (tracing_mode_flag || count > 5) {
- const error = get_stack('updated at');
+ const error = get_error('updated at');
if (error !== null) {
let entry = source.updated.get(error.stack);
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 4e82950782..100804a974 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -33,7 +33,8 @@ import {
update_derived
} from './reactivity/deriveds.js';
import { async_mode_flag, tracing_mode_flag } from '../flags/index.js';
-import { tracing_expressions, get_stack } from './dev/tracing.js';
+import { tracing_expressions } from './dev/tracing.js';
+import { get_error } from '../shared/dev.js';
import {
component_context,
dev_current_component_function,
@@ -554,7 +555,7 @@ export function get(signal) {
// if (!tracking && !untracking && !was_read) {
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
- // var trace = get_stack('traced at');
+ // var trace = get_error('traced at');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
@@ -573,7 +574,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
- var trace = get_stack('traced at');
+ var trace = get_error('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index c1003ecc1a..5c682ed140 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -2,6 +2,15 @@ import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js';
+declare global {
+ interface Window {
+ __svelte?: {
+ /** hydratables */
+ h?: Map;
+ };
+ }
+}
+
type EventCallback = (event: Event) => boolean;
export type EventCallbackMap = Record;
diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 1081ef5861..a9a50c57d6 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -87,6 +87,18 @@ export function event_handler_invalid(handler, suggestion) {
}
}
+/**
+ * Expected to find a hydratable with key `%key%` during hydration, but did not.
+ * @param {string} key
+ */
+export function hydratable_missing_but_expected(key) {
+ if (DEV) {
+ console.warn(`%c[svelte] hydratable_missing_but_expected\n%cExpected to find a hydratable with key \`${key}\` during hydration, but did not.\nhttps://svelte.dev/e/hydratable_missing_but_expected`, bold, normal);
+ } else {
+ console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`);
+ }
+}
+
/**
* The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
* @param {string} attribute
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index 1211670f94..4a6cdb8cf6 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -4,6 +4,7 @@ import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
+import { get_stack } from '../shared/dev.js';
import { set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import { Renderer } from './renderer.js';
@@ -98,3 +99,12 @@ export function validate_snippet_args(renderer) {
e.invalid_snippet_arguments();
}
}
+
+export function get_user_code_location() {
+ const stack = get_stack();
+
+ return stack
+ .filter((line) => line.trim().startsWith('at '))
+ .map((line) => line.replace(/\((.*):\d+:\d+\)$/, (_, file) => `(${file})`))
+ .join('\n');
+}
diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js
index bde49fe935..3d6c0ffbdd 100644
--- a/packages/svelte/src/internal/server/errors.js
+++ b/packages/svelte/src/internal/server/errors.js
@@ -2,6 +2,18 @@
export * from '../shared/errors.js';
+/**
+ * The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
+ * @returns {never}
+ */
+export function async_local_storage_unavailable() {
+ const error = new Error(`async_local_storage_unavailable\nThe node API \`AsyncLocalStorage\` is not available, but is required to use async server rendering.\nhttps://svelte.dev/e/async_local_storage_unavailable`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+}
+
/**
* Encountered asynchronous work while rendering synchronously.
* @returns {never}
@@ -26,6 +38,48 @@ export function html_deprecated() {
throw error;
}
+/**
+ * Attempted to set `hydratable` with key `%key%` twice with different values.
+ *
+ * %stack%
+ * @param {string} key
+ * @param {string} stack
+ * @returns {never}
+ */
+export function hydratable_clobbering(key, stack) {
+ const error = new Error(`hydratable_clobbering\nAttempted to set \`hydratable\` with key \`${key}\` twice with different values.
+
+${stack}\nhttps://svelte.dev/e/hydratable_clobbering`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+}
+
+/**
+ * Failed to serialize `hydratable` data for key `%key%`.
+ *
+ * `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
+ *
+ * Cause:
+ * %stack%
+ * @param {string} key
+ * @param {string} stack
+ * @returns {never}
+ */
+export function hydratable_serialization_failed(key, stack) {
+ const error = new Error(`hydratable_serialization_failed\nFailed to serialize \`hydratable\` data for key \`${key}\`.
+
+\`hydratable\` can serialize anything [\`uneval\` from \`devalue\`](https://npmjs.com/package/uneval) can, plus Promises.
+
+Cause:
+${stack}\nhttps://svelte.dev/e/hydratable_serialization_failed`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+}
+
/**
* `%name%(...)` is not available on the server
* @param {string} name
@@ -36,5 +90,17 @@ export function lifecycle_function_unavailable(name) {
error.name = 'Svelte error';
+ throw error;
+}
+
+/**
+ * Could not resolve `render` context.
+ * @returns {never}
+ */
+export function server_context_required() {
+ const error = new Error(`server_context_required\nCould not resolve \`render\` context.\nhttps://svelte.dev/e/server_context_required`);
+
+ error.name = 'Svelte error';
+
throw error;
}
\ No newline at end of file
diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js
new file mode 100644
index 0000000000..59fa97da4c
--- /dev/null
+++ b/packages/svelte/src/internal/server/hydratable.js
@@ -0,0 +1,136 @@
+/** @import { HydratableLookupEntry } from '#server' */
+import { async_mode_flag } from '../flags/index.js';
+import { get_render_context } from './render-context.js';
+import * as e from './errors.js';
+import * as devalue from 'devalue';
+import { get_stack } from '../shared/dev.js';
+import { DEV } from 'esm-env';
+import { get_user_code_location } from './dev.js';
+
+/**
+ * @template T
+ * @param {string} key
+ * @param {() => T} fn
+ * @returns {T}
+ */
+export function hydratable(key, fn) {
+ if (!async_mode_flag) {
+ e.experimental_async_required('hydratable');
+ }
+
+ const { hydratable } = get_render_context();
+
+ let entry = hydratable.lookup.get(key);
+
+ if (entry !== undefined) {
+ if (DEV) {
+ const comparison = compare(key, entry, encode(key, fn()));
+ comparison.catch(() => {});
+ hydratable.comparisons.push(comparison);
+ }
+
+ return /** @type {T} */ (entry.value);
+ }
+
+ const value = fn();
+
+ entry = encode(key, value, hydratable.unresolved_promises);
+ hydratable.lookup.set(key, entry);
+
+ return value;
+}
+
+/**
+ * @param {string} key
+ * @param {any} value
+ * @param {Map, string>} [unresolved]
+ */
+function encode(key, value, unresolved) {
+ /** @type {HydratableLookupEntry} */
+ const entry = { value, serialized: '' };
+
+ if (DEV) {
+ entry.stack = get_user_code_location();
+ }
+
+ let uid = 1;
+
+ entry.serialized = devalue.uneval(entry.value, (value, uneval) => {
+ if (value instanceof Promise) {
+ const p = value
+ .then((v) => `r(${uneval(v)})`)
+ .catch((devalue_error) =>
+ e.hydratable_serialization_failed(
+ key,
+ serialization_stack(entry.stack, devalue_error?.stack)
+ )
+ );
+
+ // prevent unhandled rejections from crashing the server
+ p.catch(() => {});
+
+ // track which promises are still resolving when render is complete
+ unresolved?.set(p, key);
+ p.finally(() => unresolved?.delete(p));
+
+ // we serialize promises as `"${i}"`, because it's impossible for that string
+ // to occur 'naturally' (since the quote marks would have to be escaped)
+ const placeholder = `"${uid++}"`;
+
+ (entry.promises ??= []).push(
+ p.then((s) => {
+ entry.serialized = entry.serialized.replace(placeholder, s);
+ })
+ );
+
+ return placeholder;
+ }
+ });
+
+ return entry;
+}
+
+/**
+ * @param {string} key
+ * @param {HydratableLookupEntry} a
+ * @param {HydratableLookupEntry} b
+ */
+async function compare(key, a, b) {
+ // note: these need to be loops (as opposed to Promise.all) because
+ // additional promises can get pushed to them while we're awaiting
+ // an earlier one
+ for (const p of a?.promises ?? []) {
+ await p;
+ }
+
+ for (const p of b?.promises ?? []) {
+ await p;
+ }
+
+ if (a.serialized !== b.serialized) {
+ const a_stack = /** @type {string} */ (a.stack);
+ const b_stack = /** @type {string} */ (b.stack);
+
+ const stack =
+ a_stack === b_stack
+ ? `Occurred at:\n${a_stack}`
+ : `First occurrence at:\n${a_stack}\n\nSecond occurrence at:\n${b_stack}`;
+
+ e.hydratable_clobbering(key, stack);
+ }
+}
+
+/**
+ * @param {string | undefined} root_stack
+ * @param {string | undefined} uneval_stack
+ */
+function serialization_stack(root_stack, uneval_stack) {
+ let out = '';
+ if (root_stack) {
+ out += root_stack + '\n';
+ }
+ if (uneval_stack) {
+ out += 'Caused by:\n' + uneval_stack + '\n';
+ }
+ return out || '';
+}
diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js
new file mode 100644
index 0000000000..a33fff69d3
--- /dev/null
+++ b/packages/svelte/src/internal/server/render-context.js
@@ -0,0 +1,74 @@
+// @ts-ignore -- we don't include node types in the production build
+/** @import { AsyncLocalStorage } from 'node:async_hooks' */
+/** @import { RenderContext } from '#server' */
+
+import { deferred } from '../shared/utils.js';
+import * as e from './errors.js';
+
+/** @type {Promise | null} */
+let current_render = null;
+
+/** @type {RenderContext | null} */
+let context = null;
+
+/** @returns {RenderContext} */
+export function get_render_context() {
+ const store = context ?? als?.getStore();
+
+ if (!store) {
+ e.server_context_required();
+ }
+
+ return store;
+}
+
+/**
+ * @template T
+ * @param {() => Promise} fn
+ * @returns {Promise}
+ */
+export async function with_render_context(fn) {
+ context = {
+ hydratable: {
+ lookup: new Map(),
+ comparisons: [],
+ unresolved_promises: new Map()
+ }
+ };
+
+ if (in_webcontainer()) {
+ const { promise, resolve } = deferred();
+ const previous_render = current_render;
+ current_render = promise;
+ await previous_render;
+ return fn().finally(resolve);
+ }
+
+ try {
+ if (als === null) {
+ e.async_local_storage_unavailable();
+ }
+ return als.run(context, fn);
+ } finally {
+ context = null;
+ }
+}
+
+/** @type {AsyncLocalStorage | null} */
+let als = null;
+
+export async function init_render_context() {
+ if (als !== null) return;
+ try {
+ // @ts-ignore -- we don't include node types in the production build
+ const { AsyncLocalStorage } = await import('node:async_hooks');
+ als = new AsyncLocalStorage();
+ } catch {}
+}
+
+// this has to be a function because rollup won't treeshake it if it's a constant
+function in_webcontainer() {
+ // @ts-ignore -- this will fail when we run typecheck because we exclude node types
+ // eslint-disable-next-line n/prefer-global/process
+ return !!globalThis.process?.versions?.webcontainer;
+}
diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js
index 479175c2eb..0cfb1a7a93 100644
--- a/packages/svelte/src/internal/server/renderer.js
+++ b/packages/svelte/src/internal/server/renderer.js
@@ -1,18 +1,19 @@
/** @import { Component } from 'svelte' */
-/** @import { RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
+/** @import { HydratableContext, RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
+/** @import { MaybePromise } from '#shared' */
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 { pop, push, set_ssr_context, ssr_context, save } 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';
+import { get_render_context, with_render_context, init_render_context } from './render-context.js';
+import { DEV } from 'esm-env';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
-/**
- * @template T
- * @typedef {T | Promise} MaybePromise
- */
+
/**
* @typedef {string | Renderer} RendererItem
*/
@@ -423,7 +424,9 @@ export class Renderer {
});
return Promise.resolve(user_result);
}
- async ??= Renderer.#render_async(component, options);
+ async ??= init_render_context().then(() =>
+ with_render_context(() => Renderer.#render_async(component, options))
+ );
return async.then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
@@ -515,15 +518,19 @@ export class Renderer {
* @returns {Promise}
*/
static async #render_async(component, options) {
- var previous_context = ssr_context;
+ const previous_context = ssr_context;
+
try {
const renderer = Renderer.#open_render('async', component, options);
-
const content = await renderer.#collect_content_async();
+ const hydratables = await renderer.#collect_hydratables();
+ if (hydratables !== null) {
+ content.head = hydratables + content.head;
+ }
return Renderer.#close_render(content, renderer);
} finally {
- abort();
set_ssr_context(previous_context);
+ abort();
}
}
@@ -564,6 +571,23 @@ export class Renderer {
return content;
}
+ async #collect_hydratables() {
+ const ctx = get_render_context().hydratable;
+
+ for (const [_, key] of ctx.unresolved_promises) {
+ // this is a problem -- it means we've finished the render but we're still waiting on a promise to resolve so we can
+ // serialize it, so we're blocking the response on useless content.
+ w.unresolved_hydratable(key, ctx.lookup.get(key)?.stack ?? '');
+ }
+
+ for (const comparison of ctx.comparisons) {
+ // these reject if there's a mismatch
+ await comparison;
+ }
+
+ return await Renderer.#hydratable_block(ctx);
+ }
+
/**
* @template {Record} Props
* @param {'sync' | 'async'} mode
@@ -617,6 +641,48 @@ export class Renderer {
body
};
}
+
+ /**
+ * @param {HydratableContext} ctx
+ */
+ static async #hydratable_block(ctx) {
+ if (ctx.lookup.size === 0) {
+ return null;
+ }
+
+ let entries = [];
+ let has_promises = false;
+
+ for (const [k, v] of ctx.lookup) {
+ if (v.promises) {
+ has_promises = true;
+ for (const p of v.promises) await p;
+ }
+
+ entries.push(`[${JSON.stringify(k)},${v.serialized}]`);
+ }
+
+ let prelude = `const h = (window.__svelte ??= {}).h ??= new Map();`;
+
+ if (has_promises) {
+ prelude = `const r = (v) => Promise.resolve(v);
+ ${prelude}`;
+ }
+
+ // TODO csp -- have discussed but not implemented
+ return `
+ `;
+ }
}
export class SSRState {
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index 53cefabc69..05ee34fb17 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -1,3 +1,4 @@
+import type { MaybePromise } from '#shared';
import type { Element } from './dev';
import type { Renderer } from './renderer';
@@ -14,6 +15,24 @@ export interface SSRContext {
element?: Element;
}
+export interface HydratableLookupEntry {
+ value: unknown;
+ serialized: string;
+ promises?: Array>;
+ /** dev-only */
+ stack?: string;
+}
+
+export interface HydratableContext {
+ lookup: Map;
+ comparisons: Promise[];
+ unresolved_promises: Map, string>;
+}
+
+export interface RenderContext {
+ hydratable: HydratableContext;
+}
+
export interface SyncRenderOutput {
/** HTML that goes into the `` */
head: string;
diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js
index d4ee7a86c2..fc44a086af 100644
--- a/packages/svelte/src/internal/server/warnings.js
+++ b/packages/svelte/src/internal/server/warnings.js
@@ -3,4 +3,27 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
-var normal = 'font-weight: normal';
\ No newline at end of file
+var normal = 'font-weight: normal';
+
+/**
+ * A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
+ *
+ * The `hydratable` was initialized in:
+ * %stack%
+ * @param {string} key
+ * @param {string} stack
+ */
+export function unresolved_hydratable(key, stack) {
+ if (DEV) {
+ console.warn(
+ `%c[svelte] unresolved_hydratable\n%cA \`hydratable\` value with key \`${key}\` was created, but at least part of it was not used during the render.
+
+The \`hydratable\` was initialized in:
+${stack}\nhttps://svelte.dev/e/unresolved_hydratable`,
+ bold,
+ normal
+ );
+ } else {
+ console.warn(`https://svelte.dev/e/unresolved_hydratable`);
+ }
+}
\ No newline at end of file
diff --git a/packages/svelte/src/internal/shared/dev.js b/packages/svelte/src/internal/shared/dev.js
new file mode 100644
index 0000000000..aadb3c7e6d
--- /dev/null
+++ b/packages/svelte/src/internal/shared/dev.js
@@ -0,0 +1,65 @@
+import { define_property } from './utils.js';
+
+/**
+ * @param {string} label
+ * @returns {Error & { stack: string } | null}
+ */
+export function get_error(label) {
+ const error = new Error();
+ const stack = get_stack();
+
+ if (stack.length === 0) {
+ return null;
+ }
+
+ stack.unshift('\n');
+
+ define_property(error, 'stack', {
+ value: stack.join('\n')
+ });
+
+ define_property(error, 'name', {
+ value: label
+ });
+
+ return /** @type {Error & { stack: string }} */ (error);
+}
+
+/**
+ * @returns {string[]}
+ */
+export function get_stack() {
+ // @ts-ignore - doesn't exist everywhere
+ const limit = Error.stackTraceLimit;
+ // @ts-ignore - doesn't exist everywhere
+ Error.stackTraceLimit = Infinity;
+ const stack = new Error().stack;
+ // @ts-ignore - doesn't exist everywhere
+ Error.stackTraceLimit = limit;
+
+ if (!stack) return [];
+
+ const lines = stack.split('\n');
+ const new_lines = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const posixified = line.replaceAll('\\', '/');
+
+ if (line.trim() === 'Error') {
+ continue;
+ }
+
+ if (line.includes('validate_each_keys')) {
+ return [];
+ }
+
+ if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
+ continue;
+ }
+
+ new_lines.push(line);
+ }
+
+ return new_lines;
+}
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 669cdd96a7..b13a65b598 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -2,6 +2,23 @@
import { DEV } from 'esm-env';
+/**
+ * Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
+ * @param {string} name
+ * @returns {never}
+ */
+export function experimental_async_required(name) {
+ if (DEV) {
+ const error = new Error(`experimental_async_required\nCannot use \`${name}(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_required`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/experimental_async_required`);
+ }
+}
+
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}
diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts
index 4deeb76b2f..3374d7bc16 100644
--- a/packages/svelte/src/internal/shared/types.d.ts
+++ b/packages/svelte/src/internal/shared/types.d.ts
@@ -8,3 +8,5 @@ export type Getters = {
};
export type Snapshot = ReturnType>;
+
+export type MaybePromise = T | Promise;
diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js
index 10f8597520..659cd10040 100644
--- a/packages/svelte/src/internal/shared/utils.js
+++ b/packages/svelte/src/internal/shared/utils.js
@@ -48,7 +48,7 @@ export function run_all(arr) {
/**
* TODO replace with Promise.withResolvers once supported widely enough
- * @template T
+ * @template [T=void]
*/
export function deferred() {
/** @type {(value: T) => void} */
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index e0f10ca2dd..13975c68ee 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -5,7 +5,7 @@ import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server';
-import { afterAll, assert, beforeAll } from 'vitest';
+import { afterAll, assert, beforeAll, beforeEach } from 'vitest';
import { async_mode, compile_directory, fragments } from '../helpers.js';
import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
@@ -86,6 +86,7 @@ export interface RuntimeTest = Record void | Promise;
test_ssr?: (args: {
logs: any[];
+ warnings: any[];
assert: Assert;
variant: 'ssr' | 'async-ssr';
}) => void | Promise;
@@ -102,6 +103,14 @@ export interface RuntimeTest = Record;
+ }
+ | undefined;
+}
+
let unhandled_rejection: Error | null = null;
function unhandled_rejection_handler(err: Error) {
@@ -115,6 +124,10 @@ beforeAll(() => {
process.prependListener('unhandledRejection', unhandled_rejection_handler);
});
+beforeEach(() => {
+ delete globalThis?.__svelte?.h;
+});
+
afterAll(() => {
process.removeListener('unhandledRejection', unhandled_rejection_handler);
});
@@ -252,7 +265,16 @@ async function run_test_variant(
i++;
}
- if (str.slice(0, i).includes('logs')) {
+ let ssr_str = config.test_ssr?.toString() ?? '';
+ let sn = 0;
+ let si = 0;
+ while (si < ssr_str.length) {
+ if (ssr_str[si] === '(') sn++;
+ if (ssr_str[si] === ')' && --sn === 0) break;
+ si++;
+ }
+
+ if (str.slice(0, i).includes('logs') || ssr_str.slice(0, si).includes('logs')) {
// eslint-disable-next-line no-console
console.log = (...args) => {
logs.push(...args);
@@ -263,7 +285,11 @@ async function run_test_variant(
manual_hydrate = true;
}
- if (str.slice(0, i).includes('warnings') || config.warnings) {
+ if (
+ str.slice(0, i).includes('warnings') ||
+ config.warnings ||
+ ssr_str.slice(0, si).includes('warnings')
+ ) {
// eslint-disable-next-line no-console
console.warn = (...args) => {
if (typeof args[0] === 'string' && args[0].startsWith('%c[svelte]')) {
@@ -383,6 +409,7 @@ async function run_test_variant(
if (config.test_ssr) {
await config.test_ssr({
logs,
+ warnings,
// @ts-expect-error
assert: {
...assert,
@@ -403,6 +430,15 @@ async function run_test_variant(
throw new Error('Ensure dom mode is skipped');
};
+ const run_hydratables_init = () => {
+ if (variant !== 'hydrate') return;
+ const script = [...document.head.querySelectorAll('script').values()].find((script) =>
+ script.textContent?.includes('window.__svelte ??= {}')
+ )?.textContent;
+ if (!script) return;
+ (0, eval)(script);
+ };
+
if (runes) {
props = proxy({ ...(config.props || {}) });
@@ -411,6 +447,7 @@ async function run_test_variant(
if (manual_hydrate && variant === 'hydrate') {
hydrate_fn = () => {
+ run_hydratables_init();
instance = hydrate(mod.default, {
target,
props,
@@ -419,6 +456,7 @@ async function run_test_variant(
});
};
} else {
+ run_hydratables_init();
const render = variant === 'hydrate' ? hydrate : mount;
instance = render(mod.default, {
target,
@@ -428,6 +466,7 @@ async function run_test_variant(
});
}
} else {
+ run_hydratables_init();
instance = createClassComponent({
component: mod.default,
props: config.props,
diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js
new file mode 100644
index 0000000000..0ac5333c4a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js
@@ -0,0 +1,24 @@
+import { tick } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ skip_no_async: true,
+ skip_mode: ['server'],
+
+ server_props: { environment: 'server' },
+ ssrHtml: '
The current environment is: server
',
+
+ props: { environment: 'browser' },
+
+ async test({ assert, target, variant }) {
+ // make sure hydration has a chance to finish
+ await tick();
+ const p = target.querySelector('p');
+ ok(p);
+ if (variant === 'hydrate') {
+ assert.htmlEqual(p.outerHTML, '
did you ever hear the tragedy of darth plagueis the wise?
Loading...
',
+
+ test_ssr({ assert, warnings }) {
+ assert.strictEqual(warnings.length, 1);
+ // for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it
+ assert.include(warnings[0], 'A `hydratable` value with key `partially_used`');
+ },
+
+ async test({ assert, target }) {
+ // make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used)
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
did you ever hear the tragedy of darth plagueis the wise?
',
+
+ test_ssr({ assert, warnings }) {
+ assert.strictEqual(warnings.length, 1);
+ // for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it
+ assert.include(warnings[0], 'A `hydratable` value with key `unused_key`');
+ },
+
+ async test({ assert, target }) {
+ // make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used)
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
did you ever hear the tragedy of darth plagueis the wise?
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js
index a1b52a2df9..2d1b6be570 100644
--- a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js
+++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js
@@ -1,6 +1,7 @@
import { test } from '../../test';
export default test({
+ skip: true, // TODO it appears there might be an actual bug here; the promise isn't ever actually awaited in spite of being awaited in the component
mode: ['async'],
error: 'lifecycle_outside_component'
});
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/_config.js b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/_config.js
new file mode 100644
index 0000000000..05de37a8bd
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/_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/hydratable-clobbering-but-ok/_expected.html b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/_expected.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/main.svelte b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/main.svelte
new file mode 100644
index 0000000000..87a31a8359
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-but-ok/main.svelte
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/_config.js b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/_config.js
new file mode 100644
index 0000000000..404260cc66
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['async'],
+ error: 'hydratable_clobbering'
+});
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/main.svelte b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/main.svelte
new file mode 100644
index 0000000000..358488c3ac
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering-complicated/main.svelte
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/_config.js b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/_config.js
new file mode 100644
index 0000000000..404260cc66
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['async'],
+ error: 'hydratable_clobbering'
+});
diff --git a/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/main.svelte b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/main.svelte
new file mode 100644
index 0000000000..764c2c2415
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/hydratable-clobbering/main.svelte
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
index 6325ea7d0e..12deae1e3e 100644
--- a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
+++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
@@ -1,9 +1,11 @@
import { test } from '../../test';
export default test({
+ skip: true, // TODO: This test actually works, but the error message is printed, not thrown, so we need to have a way to test for that
compileOptions: {
dev: true
},
+
error:
'node_invalid_placement_ssr: `
` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:2:1) cannot be a child of `
` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
});
diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts
index 7eede332a7..4b33685608 100644
--- a/packages/svelte/tests/server-side-rendering/test.ts
+++ b/packages/svelte/tests/server-side-rendering/test.ts
@@ -73,6 +73,7 @@ const { test, run } = suite_with_variants void): void;
+ export function hydratable(key: string, fn: () => T): T;
/**
* Create a snippet programmatically
* */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0afaef0ceb..0b1f57213d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -89,6 +89,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ devalue:
+ specifier: ^5.5.0
+ version: 5.5.0
esm-env:
specifier: ^1.2.1
version: 1.2.1
@@ -1515,6 +1518,9 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
+ devalue@5.5.0:
+ resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==}
+
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -3990,6 +3996,8 @@ snapshots:
detect-libc@1.0.3:
optional: true
+ devalue@5.5.0: {}
+
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
From c6f99e6399fc5ebe66b0a4ddb4b68abbee92ea28 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 24 Nov 2025 21:22:58 -0500
Subject: [PATCH 40/88] Version Packages (#17234)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/big-masks-shave.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/big-masks-shave.md
diff --git a/.changeset/big-masks-shave.md b/.changeset/big-masks-shave.md
deleted file mode 100644
index 96c18fdb6c..0000000000
--- a/.changeset/big-masks-shave.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': minor
----
-
-feat: `hydratable` API
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 36ff8010ab..f1f33f6c00 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
# svelte
+## 5.44.0
+
+### Minor Changes
+
+- feat: `hydratable` API ([#17154](https://github.com/sveltejs/svelte/pull/17154))
+
## 5.43.15
### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 264c672828..416493faf6 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.43.15",
+ "version": "5.44.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index f7ad2de61c..b95eb1f2b2 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.43.15';
+export const VERSION = '5.44.0';
export const PUBLIC_VERSION = '5';
From 99362b029bdaae0abba8f0f223615bf62cac7858 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 25 Nov 2025 09:46:54 -0500
Subject: [PATCH 41/88] =?UTF-8?q?=E2=80=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
documentation/docs/06-runtime/05-hydratable.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/documentation/docs/06-runtime/05-hydratable.md b/documentation/docs/06-runtime/05-hydratable.md
index 298f653a7e..a5302f264d 100644
--- a/documentation/docs/06-runtime/05-hydratable.md
+++ b/documentation/docs/06-runtime/05-hydratable.md
@@ -17,7 +17,7 @@ In Svelte, when you want to render asynchronous content data on the server, you
{user.name}
```
-That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often -- it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
+That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
To fix the example above:
From 707b6b8c20d2f8f969d688c8c23d9f799b1b101f Mon Sep 17 00:00:00 2001
From: t11r <1674104+t11r@users.noreply.github.com>
Date: Tue, 25 Nov 2025 21:42:20 +0100
Subject: [PATCH 42/88] docs: improve typography (#17222)
Some characters, like quotes and ellipses, are automatically replaced with their typographic counterparts on the website, but dashes remain as-is, and we want the pretty ones.
---
documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +-
documentation/docs/07-misc/02-testing.md | 2 +-
documentation/docs/07-misc/06-v4-migration-guide.md | 6 +++---
documentation/docs/07-misc/07-v5-migration-guide.md | 10 +++++-----
documentation/docs/07-misc/99-faq.md | 2 +-
documentation/docs/99-legacy/10-legacy-on.md | 2 +-
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md
index f7a78beec9..95e1c260c1 100644
--- a/documentation/docs/06-runtime/03-lifecycle-hooks.md
+++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md
@@ -94,7 +94,7 @@ Svelte 4 contained hooks that ran before and after the component as a whole was
```
-Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead - these runes offer more granular control and only react to the changes you're actually interested in.
+Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead — these runes offer more granular control and only react to the changes you're actually interested in.
### Chat window example
diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md
index 4807fc8f0c..85db7fc01f 100644
--- a/documentation/docs/07-misc/02-testing.md
+++ b/documentation/docs/07-misc/02-testing.md
@@ -294,7 +294,7 @@ E2E (short for 'end to end') tests allow you to test your full application throu
You can use the Svelte CLI to [setup Playwright](/docs/cli/playwright) either during project creation or later on. You can also [set it up with `npm init playwright`](https://playwright.dev/docs/intro). Additionally, you may also want to install an IDE plugin such as [the VS Code extension](https://playwright.dev/docs/getting-started-vscode) to be able to execute tests from inside your IDE.
-If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:
+If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests — mainly starting your application at a certain port. For example:
```js
/// file: playwright.config.js
diff --git a/documentation/docs/07-misc/06-v4-migration-guide.md b/documentation/docs/07-misc/06-v4-migration-guide.md
index 484931f20a..dbb1cac215 100644
--- a/documentation/docs/07-misc/06-v4-migration-guide.md
+++ b/documentation/docs/07-misc/06-v4-migration-guide.md
@@ -137,7 +137,7 @@ Transitions are now local by default to prevent confusion around page navigation
{/if}
```
-To make transitions global, add the `|global` modifier - then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
+To make transitions global, add the `|global` modifier — then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
## Default slot bindings
@@ -150,10 +150,10 @@ Default slot bindings are no longer exposed to named slots and vice versa:
- count in default slot - is available: {count}
+ count in default slot — is available: {count}
- count in bar slot - is not available: {count}
+ count in bar slot — is not available: {count}
```
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index 37da3b7b23..40cbc3bd9e 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -4,7 +4,7 @@ title: Svelte 5 migration guide
Version 5 comes with an overhauled syntax and reactivity system. While it may look different at first, you'll soon notice many similarities. This guide goes over the changes in detail and shows you how to upgrade. Along with it, we also provide information on _why_ we did these changes.
-You don't have to migrate to the new syntax right away - Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
+You don't have to migrate to the new syntax right away — Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
## Reactivity syntax changes
@@ -23,7 +23,7 @@ In Svelte 4, a `let` declaration at the top level of a component was implicitly
Nothing else changes. `count` is still the number itself, and you read and write directly to it, without a wrapper like `.value` or `getCount()`.
> [!DETAILS] Why we did this
-> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
+> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained — a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
### $: → $derived/$effect
@@ -120,7 +120,7 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
## Event changes
-Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words - remove the colon):
+Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words — remove the colon):
```svelte
+
+
+
+
+
Keyed
+{#each items as item, index (item)}
+
Item: {item.t}. Index: {index}
+{/each}
+
+
Unkeyed
+{#each items as item, index}
+
Item: {item.t}. Index: {index}
+{/each}
From 5546272988930b47bf173946012f3fd53054dab7 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Tue, 25 Nov 2025 22:42:43 +0100
Subject: [PATCH 45/88] chore: test component exports + async (#17241)
Regression-test, this works already. Closes #16657
---
.../samples/async-component-exports/Child.svelte | 9 +++++++++
.../samples/async-component-exports/_config.js | 12 ++++++++++++
.../samples/async-component-exports/main.svelte | 11 +++++++++++
3 files changed, 32 insertions(+)
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte
new file mode 100644
index 0000000000..d5ad4754fb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js b/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js
new file mode 100644
index 0000000000..a4e4f24360
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js
@@ -0,0 +1,12 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ await tick();
+ const [log] = target.querySelectorAll('button');
+
+ log.click();
+ assert.deepEqual(logs, ['foo', 'bar']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte
new file mode 100644
index 0000000000..9e4d07ddfe
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte
@@ -0,0 +1,11 @@
+
+
+
+
From e3acf5deeae6cb8f872f3cb418de23f29429dc08 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 25 Nov 2025 16:50:39 -0500
Subject: [PATCH 46/88] chore: better log_effect_tree (#17243)
* chore: better log_effect_tree
* untrack
---------
Co-authored-by: Simon Holthausen
---
.../svelte/src/internal/client/dev/debug.js | 27 +++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js
index 2714a3af1f..ebb612cb61 100644
--- a/packages/svelte/src/internal/client/dev/debug.js
+++ b/packages/svelte/src/internal/client/dev/debug.js
@@ -12,6 +12,8 @@ import {
RENDER_EFFECT,
ROOT_EFFECT
} from '#client/constants';
+import { snapshot } from '../../shared/clone.js';
+import { untrack } from '../runtime.js';
/**
*
@@ -84,6 +86,16 @@ export function log_effect_tree(effect, depth = 0) {
console.groupEnd();
}
+ if (effect.nodes_start && effect.nodes_end) {
+ // eslint-disable-next-line no-console
+ console.log(effect.nodes_start);
+
+ if (effect.nodes_start !== effect.nodes_end) {
+ // eslint-disable-next-line no-console
+ console.log(effect.nodes_end);
+ }
+ }
+
let child = effect.first;
while (child !== null) {
log_effect_tree(child, depth + 1);
@@ -103,7 +115,13 @@ function log_dep(dep) {
const derived = /** @type {Derived} */ (dep);
// eslint-disable-next-line no-console
- console.groupCollapsed('%cderived', 'font-weight: normal', derived.v);
+ console.groupCollapsed(
+ `%c$derived %c${dep.label ?? ''}`,
+ 'font-weight: bold; color: CornflowerBlue',
+ 'font-weight: normal',
+ untrack(() => snapshot(derived.v))
+ );
+
if (derived.deps) {
for (const d of derived.deps) {
log_dep(d);
@@ -114,6 +132,11 @@ function log_dep(dep) {
console.groupEnd();
} else {
// eslint-disable-next-line no-console
- console.log('state', dep.v);
+ console.log(
+ `%c$state %c${dep.label ?? ''}`,
+ 'font-weight: bold; color: CornflowerBlue',
+ 'font-weight: normal',
+ untrack(() => snapshot(dep.v))
+ );
}
}
From d0e61bc2529db2f83998f6a9ca6b02f841ecfcde Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Tue, 25 Nov 2025 22:52:02 +0100
Subject: [PATCH 47/88] fix: ensure each block animations don't mess with
transitions (#17238)
This ensures that an animation does not run when the element is first transitioning in. Else the animation would mess with the transition (overriding its animation basically). Due to our test setup it's not testable but I veryfied it fixes #17181 (tested all reproductions in there)
---
.changeset/tiny-loops-wonder.md | 5 +++++
packages/svelte/src/internal/client/dom/blocks/each.js | 8 ++++++--
2 files changed, 11 insertions(+), 2 deletions(-)
create mode 100644 .changeset/tiny-loops-wonder.md
diff --git a/.changeset/tiny-loops-wonder.md b/.changeset/tiny-loops-wonder.md
new file mode 100644
index 0000000000..49ac92947c
--- /dev/null
+++ b/.changeset/tiny-loops-wonder.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure each block animations don't mess with transitions
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 77e669aa22..0147de3c3d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -394,8 +394,12 @@ function reconcile(state, array, anchor, flags, get_key) {
key = get_key(value, i);
item = /** @type {EachItem} */ (items.get(key));
- item.a?.measure();
- (to_animate ??= new Set()).add(item);
+ // offscreen == coming in now, no animation in that case,
+ // else this would happen https://github.com/sveltejs/svelte/issues/17181
+ if (item.o) {
+ item.a?.measure();
+ (to_animate ??= new Set()).add(item);
+ }
}
}
From a7848c021a987d0efec1cda8d4f2a6066a39ceae Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Tue, 25 Nov 2025 22:52:26 +0100
Subject: [PATCH 48/88] fix: generate correct code for simple destructurings
(#17237)
Fixes #17236
* fix: generate correct code for simple destructurings
* add a test (existing one doesn't fail on main)
* adjust existing test so it fails on main
* slightly neater approach (with identical outcome)
---------
Co-authored-by: Rich Harris
---
.changeset/lovely-windows-shout.md | 5 ++++
.../3-transform/shared/transform-async.js | 29 +++++++++----------
.../async-derived-destructured/Child.svelte | 4 +++
.../async-derived-destructured/_config.js | 2 ++
4 files changed, 25 insertions(+), 15 deletions(-)
create mode 100644 .changeset/lovely-windows-shout.md
diff --git a/.changeset/lovely-windows-shout.md b/.changeset/lovely-windows-shout.md
new file mode 100644
index 0000000000..d0b748a876
--- /dev/null
+++ b/.changeset/lovely-windows-shout.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: generate correct code for simple destructurings
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
index 6ec7893452..22c4beb08a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
@@ -53,23 +53,22 @@ export function transform_body(instance_body, runner, transform) {
transform(b.var(s.node.id, s.node.init))
);
- if (visited.declarations.length === 1) {
- return b.thunk(
- b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0),
- s.has_await
- );
+ const statements = visited.declarations.map((node) => {
+ if (node.id.type === 'Identifier' && node.id.name.startsWith('$$d')) {
+ // this is an intermediate declaration created in VariableDeclaration.js;
+ // subsequent statements depend on it
+ return b.var(node.id, node.init);
+ }
+
+ return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
+ });
+
+ if (statements.length === 1) {
+ const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
+ return b.thunk(statement.expression, s.has_await);
}
- // if we have multiple declarations, it indicates destructuring
- return b.thunk(
- b.block([
- b.var(visited.declarations[0].id, visited.declarations[0].init),
- ...visited.declarations
- .slice(1)
- .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
- ]),
- s.has_await
- );
+ return b.thunk(b.block(statements), s.has_await);
}
if (s.node.type === 'ClassDeclaration') {
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
index 39112b12a7..fdf7184e3c 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
@@ -1,13 +1,17 @@
diff --git a/packages/svelte/tests/print/test.ts b/packages/svelte/tests/print/test.ts
new file mode 100644
index 0000000000..aa007a7a54
--- /dev/null
+++ b/packages/svelte/tests/print/test.ts
@@ -0,0 +1,30 @@
+import * as fs from 'node:fs';
+import { assert } from 'vitest';
+import { parse, print } from 'svelte/compiler';
+import { suite, type BaseTest } from '../suite.js';
+
+interface PrintTest extends BaseTest {}
+
+const { test, run } = suite(async (config, cwd) => {
+ const input = fs.readFileSync(`${cwd}/input.svelte`, 'utf-8');
+
+ const ast = parse(input, { modern: true });
+ const output = print(ast);
+ const outputCode = output.code.endsWith('\n') ? output.code : output.code + '\n';
+
+ // run `UPDATE_SNAPSHOTS=true pnpm test print` to update print tests
+ if (process.env.UPDATE_SNAPSHOTS) {
+ fs.writeFileSync(`${cwd}/output.svelte`, outputCode);
+ } else {
+ fs.writeFileSync(`${cwd}/_actual.svelte`, outputCode);
+
+ const file = `${cwd}/output.svelte`;
+
+ const expected = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
+ assert.deepEqual(outputCode.trim().replaceAll('\r', ''), expected.trim().replaceAll('\r', ''));
+ }
+});
+
+export { test };
+
+await run(__dirname);
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index f0200d09fe..8561268689 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -844,6 +844,7 @@ declare module 'svelte/compiler' {
import type { SourceMap } from 'magic-string';
import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { Location } from 'locate-character';
+ import type { default as ts } from 'esrap/languages/ts';
/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component
*
@@ -1390,7 +1391,7 @@ declare module 'svelte/compiler' {
expression: null | Expression;
}
- interface BaseElement extends BaseNode {
+ export interface BaseElement extends BaseNode {
name: string;
attributes: Array;
fragment: Fragment;
@@ -1616,6 +1617,18 @@ declare module 'svelte/compiler' {
export function preprocess(source: string, preprocessor: PreprocessorGroup | PreprocessorGroup[], options?: {
filename?: string;
} | undefined): Promise;
+ /**
+ * `print` converts a Svelte AST node back into Svelte source code.
+ * It is primarily intended for tools that parse and transform components using the compiler’s modern AST representation.
+ *
+ * `print(ast)` requires an AST node produced by parse with modern: true, or any sub-node within that modern AST.
+ * The result contains the generated source and a corresponding source map.
+ * The output is valid Svelte, but formatting details such as whitespace or quoting may differ from the original.
+ * */
+ export function print(ast: AST.SvelteNode, options?: Options | undefined): {
+ code: string;
+ map: any;
+ };
/**
* The current version, as set in package.json.
* */
@@ -1799,6 +1812,10 @@ declare module 'svelte/compiler' {
| SimpleSelector
| Declaration;
}
+ type Options = {
+ getLeadingComments?: NonNullable[0]>['getLeadingComments'] | undefined;
+ getTrailingComments?: NonNullable[0]>['getTrailingComments'] | undefined;
+ };
export {};
}
diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js
index 7ff9f7c4cd..35bffb67a2 100644
--- a/playgrounds/sandbox/run.js
+++ b/playgrounds/sandbox/run.js
@@ -3,7 +3,7 @@ import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { globSync } from 'tinyglobby';
-import { compile, compileModule, parse, migrate } from 'svelte/compiler';
+import { compile, compileModule, parse, print, migrate } from 'svelte/compiler';
// toggle these to change what gets written to sandbox/output
const AST = false;
@@ -11,6 +11,7 @@ const MIGRATE = false;
const FROM_HTML = true;
const FROM_TREE = false;
const DEV = false;
+const PRINT = false;
const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) });
@@ -71,6 +72,11 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
'\t'
)
);
+
+ if (PRINT) {
+ const printed = print(ast);
+ write(`${cwd}/output/printed/${file}`, printed.code);
+ }
}
if (MIGRATE) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0b1f57213d..bdb120600b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,8 +96,8 @@ importers:
specifier: ^1.2.1
version: 1.2.1
esrap:
- specifier: ^2.1.0
- version: 2.1.0
+ specifier: ^2.2.0
+ version: 2.2.0
is-reference:
specifier: ^3.0.3
version: 3.0.3
@@ -1178,8 +1178,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/project-service@8.46.2':
- resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==}
+ '@typescript-eslint/project-service@8.48.0':
+ resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1188,12 +1188,12 @@ packages:
resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/scope-manager@8.46.2':
- resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
+ '@typescript-eslint/scope-manager@8.48.0':
+ resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.46.2':
- resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==}
+ '@typescript-eslint/tsconfig-utils@8.48.0':
+ resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1209,8 +1209,8 @@ packages:
resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/types@8.46.2':
- resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
+ '@typescript-eslint/types@8.48.0':
+ resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.26.0':
@@ -1219,8 +1219,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/typescript-estree@8.46.2':
- resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==}
+ '@typescript-eslint/typescript-estree@8.48.0':
+ resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1232,8 +1232,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/utils@8.46.2':
- resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==}
+ '@typescript-eslint/utils@8.48.0':
+ resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -1243,8 +1243,8 @@ packages:
resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/visitor-keys@8.46.2':
- resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
+ '@typescript-eslint/visitor-keys@8.48.0':
+ resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@2.1.9':
@@ -1660,8 +1660,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'}
- esrap@2.1.0:
- resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
+ esrap@2.2.0:
+ resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@@ -3648,10 +3648,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.46.2(typescript@5.5.4)':
+ '@typescript-eslint/project-service@8.48.0(typescript@5.5.4)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.5.4)
- '@typescript-eslint/types': 8.46.2
+ '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.5.4)
+ '@typescript-eslint/types': 8.48.0
debug: 4.4.3
typescript: 5.5.4
transitivePeerDependencies:
@@ -3662,12 +3662,12 @@ snapshots:
'@typescript-eslint/types': 8.26.0
'@typescript-eslint/visitor-keys': 8.26.0
- '@typescript-eslint/scope-manager@8.46.2':
+ '@typescript-eslint/scope-manager@8.48.0':
dependencies:
- '@typescript-eslint/types': 8.46.2
- '@typescript-eslint/visitor-keys': 8.46.2
+ '@typescript-eslint/types': 8.48.0
+ '@typescript-eslint/visitor-keys': 8.48.0
- '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.5.4)':
+ '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.5.4)':
dependencies:
typescript: 5.5.4
@@ -3684,7 +3684,7 @@ snapshots:
'@typescript-eslint/types@8.26.0': {}
- '@typescript-eslint/types@8.46.2': {}
+ '@typescript-eslint/types@8.48.0': {}
'@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)':
dependencies:
@@ -3700,17 +3700,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@8.46.2(typescript@5.5.4)':
+ '@typescript-eslint/typescript-estree@8.48.0(typescript@5.5.4)':
dependencies:
- '@typescript-eslint/project-service': 8.46.2(typescript@5.5.4)
- '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.5.4)
- '@typescript-eslint/types': 8.46.2
- '@typescript-eslint/visitor-keys': 8.46.2
+ '@typescript-eslint/project-service': 8.48.0(typescript@5.5.4)
+ '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.5.4)
+ '@typescript-eslint/types': 8.48.0
+ '@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
- fast-glob: 3.3.3
- is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.3
+ tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.5.4)
typescript: 5.5.4
transitivePeerDependencies:
@@ -3727,12 +3726,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.46.2(eslint@9.9.1)(typescript@5.5.4)':
+ '@typescript-eslint/utils@8.48.0(eslint@9.9.1)(typescript@5.5.4)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1)
- '@typescript-eslint/scope-manager': 8.46.2
- '@typescript-eslint/types': 8.46.2
- '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.5.4)
+ '@typescript-eslint/scope-manager': 8.48.0
+ '@typescript-eslint/types': 8.48.0
+ '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.5.4)
eslint: 9.9.1
typescript: 5.5.4
transitivePeerDependencies:
@@ -3743,9 +3742,9 @@ snapshots:
'@typescript-eslint/types': 8.26.0
eslint-visitor-keys: 4.2.1
- '@typescript-eslint/visitor-keys@8.46.2':
+ '@typescript-eslint/visitor-keys@8.48.0':
dependencies:
- '@typescript-eslint/types': 8.46.2
+ '@typescript-eslint/types': 8.48.0
eslint-visitor-keys: 4.2.1
'@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
@@ -4145,7 +4144,7 @@ snapshots:
eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1)
- '@typescript-eslint/utils': 8.46.2(eslint@9.9.1)(typescript@5.5.4)
+ '@typescript-eslint/utils': 8.48.0(eslint@9.9.1)(typescript@5.5.4)
enhanced-resolve: 5.18.3
eslint: 9.9.1
eslint-plugin-es-x: 7.8.0(eslint@9.9.1)
@@ -4245,7 +4244,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
- esrap@2.1.0:
+ esrap@2.2.0:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
From 1bc3ae7cbfaffa45c15511ea5a7ff4f1c433d78e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 26 Nov 2025 14:05:39 -0500
Subject: [PATCH 52/88] Version Packages (#17247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/little-humans-occur.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/little-humans-occur.md
diff --git a/.changeset/little-humans-occur.md b/.changeset/little-humans-occur.md
deleted file mode 100644
index 78b94ee26e..0000000000
--- a/.changeset/little-humans-occur.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': minor
----
-
-feat: add `print(...)` function
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index fe2d5ae212..11ffd0369b 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
# svelte
+## 5.45.0
+
+### Minor Changes
+
+- feat: add `print(...)` function ([#16188](https://github.com/sveltejs/svelte/pull/16188))
+
## 5.44.1
### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 3f81fb924c..d734d75f35 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.44.1",
+ "version": "5.45.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 681a3d3487..27392e17a4 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.44.1';
+export const VERSION = '5.45.0';
export const PUBLIC_VERSION = '5';
From da4cd0e35942ba9615ba6516ffaf185b511e7b59 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Wed, 26 Nov 2025 20:06:15 +0100
Subject: [PATCH 53/88] fix: link offscreen items and last effect in each block
correctly (#17240)
* fix: link offscreen items and last effect in each block correctly
It's possible that due to how new elements are inserted into the array that `effect.last` is wrong. We need to ensure it is really the last item to keep items properly connected to the graph. In addition we link offscreen items after all onscreen items, to ensure they don't have wrong pointers.
Fixes #17201
* revert #17244
* add test
---
.changeset/great-ghosts-unite.md | 5 +++
.../src/internal/client/dom/blocks/each.js | 28 +++++++++-------
.../samples/each-updates-11/_config.js | 32 +++++++++++++++++++
.../samples/each-updates-11/main.svelte | 11 +++++++
4 files changed, 65 insertions(+), 11 deletions(-)
create mode 100644 .changeset/great-ghosts-unite.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
diff --git a/.changeset/great-ghosts-unite.md b/.changeset/great-ghosts-unite.md
new file mode 100644
index 0000000000..2973737cfd
--- /dev/null
+++ b/.changeset/great-ghosts-unite.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: link offscreen items and last effect in each block correctly
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 0147de3c3d..501577053d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -507,6 +507,8 @@ function reconcile(state, array, anchor, flags, get_key) {
current = item.next;
}
+ let has_offscreen_items = items.size > length;
+
if (current !== null || seen !== undefined) {
var to_destroy = seen === undefined ? [] : array_from(seen);
@@ -520,6 +522,8 @@ function reconcile(state, array, anchor, flags, get_key) {
var destroy_length = to_destroy.length;
+ has_offscreen_items = items.size - destroy_length > length;
+
if (destroy_length > 0) {
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
@@ -537,6 +541,18 @@ function reconcile(state, array, anchor, flags, get_key) {
}
}
+ // Append offscreen items at the end
+ if (has_offscreen_items) {
+ for (const item of items.values()) {
+ if (!item.o) {
+ link(state, prev, item);
+ prev = item;
+ }
+ }
+ }
+
+ state.effect.last = prev && prev.e;
+
if (is_animated) {
queue_micro_task(() => {
if (to_animate === undefined) return;
@@ -641,10 +657,6 @@ function link(state, prev, next) {
state.first = next;
state.effect.first = next && next.e;
} else {
- if (prev.e === state.effect.last && next !== null) {
- state.effect.last = next.e;
- }
-
if (prev.e.next) {
prev.e.next.prev = null;
}
@@ -653,13 +665,7 @@ function link(state, prev, next) {
prev.e.next = next && next.e;
}
- if (next === null) {
- state.effect.last = prev && prev.e;
- } else {
- if (next.e === state.effect.last && prev === null) {
- state.effect.last = next.e.prev;
- }
-
+ if (next !== null) {
if (next.e.prev) {
next.e.prev.next = null;
}
diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
new file mode 100644
index 0000000000..a8782d2da8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
@@ -0,0 +1,32 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [add4, add5, modify3] = target.querySelectorAll('button');
+
+ add4.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 1423`
+ );
+
+ add5.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 14523`
+ );
+
+ modify3.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 1452updated`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
new file mode 100644
index 0000000000..1dcd265093
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+{#each list as item (item.id)}
+ {item.text}
+{/each}
From 5c821c1a41886b094f9e7c66f8cecd7ce515e26e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 26 Nov 2025 20:10:54 +0100
Subject: [PATCH 54/88] Version Packages (#17253)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/great-ghosts-unite.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/great-ghosts-unite.md
diff --git a/.changeset/great-ghosts-unite.md b/.changeset/great-ghosts-unite.md
deleted file mode 100644
index 2973737cfd..0000000000
--- a/.changeset/great-ghosts-unite.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: link offscreen items and last effect in each block correctly
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 11ffd0369b..d13e3c2113 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
# svelte
+## 5.45.1
+
+### Patch Changes
+
+- fix: link offscreen items and last effect in each block correctly ([#17240](https://github.com/sveltejs/svelte/pull/17240))
+
## 5.45.0
### Minor Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index d734d75f35..9e261f36ec 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.45.0",
+ "version": "5.45.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 27392e17a4..1c3e3b7124 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.45.0';
+export const VERSION = '5.45.1';
export const PUBLIC_VERSION = '5';
From ae6004657d4f56768176cba54813bcfddb83ecea Mon Sep 17 00:00:00 2001
From: Paolo Ricciuti
Date: Wed, 26 Nov 2025 21:55:38 +0100
Subject: [PATCH 55/88] fix: array destructuring after await (#17254)
---
.changeset/green-cloths-happen.md | 5 +++++
.../compiler/phases/3-transform/shared/transform-async.js | 5 ++++-
.../samples/async-derived-destructured/Child.svelte | 5 +++++
.../samples/async-derived-destructured/_config.js | 2 ++
4 files changed, 16 insertions(+), 1 deletion(-)
create mode 100644 .changeset/green-cloths-happen.md
diff --git a/.changeset/green-cloths-happen.md b/.changeset/green-cloths-happen.md
new file mode 100644
index 0000000000..a9677c8fc1
--- /dev/null
+++ b/.changeset/green-cloths-happen.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: array destructuring after await
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
index b55356a38c..2b9c219d7d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js
@@ -54,7 +54,10 @@ export function transform_body(instance_body, runner, transform) {
);
const statements = visited.declarations.map((node) => {
- if (node.id.type === 'Identifier' && node.id.name.startsWith('$$d')) {
+ if (
+ node.id.type === 'Identifier' &&
+ (node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
+ ) {
// this is an intermediate declaration created in VariableDeclaration.js;
// subsequent statements depend on it
return b.var(node.id, node.init);
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
index fdf7184e3c..04adc8e97f 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte
@@ -1,5 +1,6 @@
@@ -15,3 +19,4 @@
{count} ** 2 = {squared}
{count} ** 3 = {cubed}
{typeof toFixed} {typeof toString}
+
{a} {b}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js
index bfd582ea97..26f1dfdeb3 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js
@@ -14,6 +14,7 @@ export default test({