diff --git a/.changeset/calm-derived-connections.md b/.changeset/calm-derived-connections.md
new file mode 100644
index 0000000000..4ce38365ff
--- /dev/null
+++ b/.changeset/calm-derived-connections.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: prevent untracked derived reads from retaining disconnected dependencies
diff --git a/.changeset/hydration-custom-element-error-boundary.md b/.changeset/hydration-custom-element-error-boundary.md
new file mode 100644
index 0000000000..f94aa6265a
--- /dev/null
+++ b/.changeset/hydration-custom-element-error-boundary.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: restore hydration state when custom element attribute updates throw
diff --git a/.changeset/major-papayas-run.md b/.changeset/major-papayas-run.md
new file mode 100644
index 0000000000..59909fb448
--- /dev/null
+++ b/.changeset/major-papayas-run.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: flush anything pending before invoking flushSync callback function
diff --git a/.changeset/major-roses-help.md b/.changeset/major-roses-help.md
new file mode 100644
index 0000000000..c3c06792a4
--- /dev/null
+++ b/.changeset/major-roses-help.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: preserve location information for `await` wrappers
diff --git a/.changeset/migrate-keep-assignment.md b/.changeset/migrate-keep-assignment.md
new file mode 100644
index 0000000000..6b5be9d6be
--- /dev/null
+++ b/.changeset/migrate-keep-assignment.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: keep a reactive assignment with no dependencies when `migrate` cannot turn it into a declaration
diff --git a/.changeset/migrate-tag-line.md b/.changeset/migrate-tag-line.md
new file mode 100644
index 0000000000..414e4a249c
--- /dev/null
+++ b/.changeset/migrate-tag-line.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't hang `migrate` on a declaration that shares a line with its script tag
diff --git a/.changeset/quiet-areas-link.md b/.changeset/quiet-areas-link.md
new file mode 100644
index 0000000000..af4e31f3cb
--- /dev/null
+++ b/.changeset/quiet-areas-link.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't warn about a redundant `link` role on `` elements without an `href`
diff --git a/.changeset/short-radios-sneeze.md b/.changeset/short-radios-sneeze.md
new file mode 100644
index 0000000000..847c3eef17
--- /dev/null
+++ b/.changeset/short-radios-sneeze.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: read batch-local array on each-block commit
diff --git a/.changeset/snippet-parameter-redeclared.md b/.changeset/snippet-parameter-redeclared.md
new file mode 100644
index 0000000000..9bb14cf3af
--- /dev/null
+++ b/.changeset/snippet-parameter-redeclared.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: error at compile time when a declaration in a snippet redeclares one of its parameters
diff --git a/.changeset/sour-lets-hide.md b/.changeset/sour-lets-hide.md
new file mode 100644
index 0000000000..6175027cff
--- /dev/null
+++ b/.changeset/sour-lets-hide.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: error when using `let:` directives on a component with a `children` snippet
diff --git a/.changeset/stale-boundary-reset.md b/.changeset/stale-boundary-reset.md
new file mode 100644
index 0000000000..1f158c5713
--- /dev/null
+++ b/.changeset/stale-boundary-reset.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ignore stale boundary reset callbacks
diff --git a/.changeset/tidy-ducks-reset.md b/.changeset/tidy-ducks-reset.md
new file mode 100644
index 0000000000..41a00cf649
--- /dev/null
+++ b/.changeset/tidy-ducks-reset.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: handle deferred transitions aborted before initialization
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 2ec8985c66..8bf17f9330 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -683,6 +683,12 @@ Cannot use `$$restProps` in runes mode
`let:` directive at invalid position
```
+### let_directive_snippet_conflict
+
+```
+Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
+```
+
### mixed_event_handler_syntaxes
```
diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md
index 7fdb56ce55..797b2c53c2 100644
--- a/packages/svelte/messages/compile-errors/template.md
+++ b/packages/svelte/messages/compile-errors/template.md
@@ -283,6 +283,10 @@ The same applies to components:
> `let:` directive at invalid position
+## let_directive_snippet_conflict
+
+> Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
+
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index f43e8cae63..98f1e79279 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -1223,6 +1223,16 @@ export function let_directive_invalid_placement(node) {
e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
}
+/**
+ * Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
+ * @param {null | number | NodeLike} node
+ * @param {string} name
+ * @returns {never}
+ */
+export function let_directive_snippet_conflict(node, name) {
+ e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
+}
+
/**
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 76c061a275..ac667bf26b 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -911,8 +911,10 @@ const instance_script = {
start = /** @type {number} */ (parent.start);
end = /** @type {number} */ (parent.end);
}
- while (state.str.original[start] !== '\n') start--;
- while (state.str.original[end] !== '\n') end++;
+ // remove the declaration's whole line, or what there is of it inside the script
+ const script = /** @type {{ start: number; end: number }} */ (state.analysis.instance.ast);
+ while (start > script.start && state.str.original[start] !== '\n') start--;
+ while (end < script.end && state.str.original[end] !== '\n') end++;
state.str.update(start, end, '');
}
},
@@ -988,8 +990,11 @@ const instance_script = {
return;
}
+ let declared = 0;
for (const binding of bindings) {
- if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
+ // a binding this statement declares needs its `let` whether or not it is reassigned
+ if (ids.includes(binding.node) || (binding.reassigned && expression_ids.length === 0)) {
+ declared++;
check_rune_binding('state');
const init =
binding.kind === 'state'
@@ -1005,7 +1010,13 @@ const instance_script = {
}
}
- if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
+ // the statement goes only once every target has a declaration standing in for it
+ if (
+ expression_ids.length === 0 &&
+ bindings.length > 0 &&
+ declared === bindings.length &&
+ bindings.every((b) => b.kind !== 'store_sub')
+ ) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/LetDirective.js
index e9a56ead0c..2eccac9d8a 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/LetDirective.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/LetDirective.js
@@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
+import { determine_slot } from '../../../utils/slot.js';
/**
* @param {AST.LetDirective} node
@@ -21,4 +22,25 @@ export function LetDirective(node, context) {
) {
e.let_directive_invalid_placement(node);
}
+
+ // an explicit `children` snippet replaces the default slot content, so the `let:`
+ // directive would never be provided. if the component has a `slot` attribute, the
+ // directive applies to the component itself instead, which is fine
+ if (
+ (parent.type === 'Component' ||
+ parent.type === 'SvelteComponent' ||
+ parent.type === 'SvelteSelf') &&
+ !determine_slot(parent) &&
+ parent.fragment.nodes.some(
+ (child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
+ )
+ ) {
+ const { expression } = node;
+ const pattern =
+ expression === null || (expression.type === 'Identifier' && expression.name === node.name)
+ ? node.name
+ : `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
+
+ e.let_directive_snippet_conflict(node, pattern);
+ }
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
index f06a41d088..c38b03bf69 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
@@ -173,8 +173,8 @@ export function check_element(node, context) {
current_role === get_implicit_role(node.name, attribute_map) &&
//
is ok because CSS list-style:none removes the semantics and this is a way to bring them back
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
- // is ok because without href the a tag doesn't have a role of link
- !(node.name === 'a' && !attribute_map.has('href'))
+ // and are ok because without href they don't have a role of link
+ !((node.name === 'a' || node.name === 'area') && !attribute_map.has('href'))
) {
w.a11y_no_redundant_roles(attribute, current_role);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js
index 9fcc33287c..a3602bbce5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js
@@ -18,7 +18,8 @@ export function AwaitExpression(node, context) {
// in dev, note which values are read inside a reactive expression,
// but don't track them
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
- return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
+ // don't use b.await here, this way we preserve location information
+ return b.call({ ...node, argument: b.call('$.track_reactivity_loss', argument) });
}
return argument === node.argument ? node : { ...node, argument };
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 77c9119364..0d8e570772 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -1343,6 +1343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
context.next({ scope: child_scope });
+
+ // the parameters and what the body declares end up in the same function
+ const body_scope = /** @type {Scope} */ (scopes.get(node.body));
+ for (const [name, binding] of body_scope.declarations) {
+ if (child_scope.declarations.has(name)) {
+ e.declaration_duplicate(binding.node, name);
+ }
+ }
},
Fragment: (node, context) => {
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 0eb1b80315..3da59eb7f7 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -17,7 +17,7 @@ if (DEV) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
- let value; // let's hope noone modifies this global, but belts and braces
+ let value; // let's hope no one modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index c6edfde933..9fea8175bd 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -131,8 +131,10 @@ export function trace(label, fn) {
}
/**
- * @param {Value} source
+ * @template {Value} T
+ * @param {T} source
* @param {string} label
+ * @returns {T}
*/
export function tag(source, label) {
source.label = label;
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 54f6cc22d0..4b0a68f83c 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -1,5 +1,11 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
-import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
+import {
+ BOUNDARY_EFFECT,
+ DESTROYED,
+ DESTROYING,
+ EFFECT_PRESERVED,
+ EFFECT_TRANSPARENT
+} from '#client/constants';
import {
HYDRATION_ERROR,
HYDRATION_START_ELSE,
@@ -236,6 +242,8 @@ export class Boundary {
var calling_on_error = false;
const reset = () => {
+ if (this.#is_destroyed()) return;
+
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
@@ -259,6 +267,8 @@ export class Boundary {
};
const invoke_onerror = () => {
+ if (this.#is_destroyed()) return;
+
try {
calling_on_error = true;
this.#props.onerror?.(error, reset);
@@ -271,6 +281,10 @@ export class Boundary {
return { reset, invoke_onerror };
}
+ #is_destroyed() {
+ return (this.#effect.f & (DESTROYED | DESTROYING)) !== 0;
+ }
+
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) return;
@@ -279,6 +293,8 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
+ if (this.#is_destroyed()) return;
+
var pop_renderer = push_renderer(this.#effect.r);
try {
@@ -488,7 +504,7 @@ export class Boundary {
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.oncommit(() => {
- this.#handle_error(error);
+ if (!this.#is_destroyed()) this.#handle_error(error);
});
} else {
this.#handle_error(error);
@@ -524,11 +540,13 @@ export class Boundary {
/** @param {unknown} transformed_error */
const handle_error_result = (transformed_error) => {
+ if (this.#is_destroyed()) return;
+
const { reset, invoke_onerror } = this.#create_reset(transformed_error);
invoke_onerror();
- if (failed) {
+ if (failed && !this.#is_destroyed()) {
this.#failed_effect = this.#run(() => {
try {
return branch(() => {
@@ -554,6 +572,8 @@ export class Boundary {
};
queue_micro_task(() => {
+ if (this.#is_destroyed()) return;
+
// Run the error through the API-level transformError transform (e.g. SvelteKit's handleError)
/** @type {unknown} */
var result;
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index d2418f9757..a4465913e5 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -1,4 +1,4 @@
-/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
+/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
@@ -255,9 +255,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
tag(each_array, '{#each ...}');
}
- /** @type {V[]} */
- var array;
-
/** @type {Map>} */
var pending = new Map();
@@ -276,6 +273,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
try {
state.pending.delete(batch);
+ // The effect doesn't necessarily re-run in a batch right before that batch commits
+ // (its view of the collection may not have changed), so we read the collection
+ // as the committing batch sees it rather than using the most recent block run's result
+ var array = get(each_array);
+
state.fallback = fallback;
reconcile(state, array, anchor, flags, get_key);
@@ -309,7 +311,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
var effect = block(() => {
- array = /** @type {V[]} */ (get(each_array));
+ var array = /** @type {V[]} */ (get(each_array));
var length = array.length;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@@ -709,7 +711,7 @@ function reconcile(state, array, anchor, flags, get_key) {
* @param {V} value
* @param {unknown} key
* @param {number} index
- * @param {(anchor: Node, item: V | Source, index: number | Value, collection: () => V[]) => void} render_fn
+ * @param {(anchor: Node, item: MaybeSource, index: MaybeSource, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}
diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js
index 4147124712..0d0eb5d867 100644
--- a/packages/svelte/src/internal/client/dom/css.js
+++ b/packages/svelte/src/internal/client/dom/css.js
@@ -12,7 +12,7 @@ export function append_styles(anchor, css) {
// Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
effect(() => {
// Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will
- // never be connected to the real dom. Therfore walk up to the branch that has created the component
+ // never be connected to the real dom. Therefore walk up to the branch that has created the component
// whose styles we want to append, and check its node instead. It will be connected by the time we get here.
anchor = active_effect?.parent?.nodes?.start ?? anchor;
var root = anchor.getRootNode();
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 1b9d259912..70be1ca7db 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -310,223 +310,225 @@ function set_attributes(
var is_option_element = node_name(element) === OPTION_TAG;
var is_select_element = node_name(element) === SELECT_TAG;
- for (var key in prev) {
- // don't null our internal $$onX listeners
- if (!(key in next) && key[0] + key[1] !== '$$') {
- next[key] = null;
+ try {
+ for (var key in prev) {
+ // don't null our internal $$onX listeners
+ if (!(key in next) && key[0] + key[1] !== '$$') {
+ next[key] = null;
+ }
}
- }
- if (next.class) {
- next.class = clsx(next.class);
- } else if (css_hash || next[CLASS]) {
- next.class = null; /* force call to set_class() */
- }
+ if (next.class) {
+ next.class = clsx(next.class);
+ } else if (css_hash || next[CLASS]) {
+ next.class = null; /* force call to set_class() */
+ }
- if (next[STYLE]) {
- next.style ??= null; /* force call to set_style() */
- }
+ if (next[STYLE]) {
+ next.style ??= null; /* force call to set_style() */
+ }
- var setters = get_setters(element);
+ var setters = get_setters(element);
- if (
- // the following logic only make sense in a dev environment
- current_renderer == null &&
- node_name(element) === INPUT_TAG &&
- 'type' in next &&
- ('value' in next || '__value' in next)
- ) {
- var type = next.type;
-
- if (type !== current.type || (type === undefined && element.hasAttribute('type'))) {
- current.type = type;
- set_attribute(element, 'type', type, skip_warning);
- }
- }
+ if (
+ // the following logic only make sense in a dev environment
+ current_renderer == null &&
+ node_name(element) === INPUT_TAG &&
+ 'type' in next &&
+ ('value' in next || '__value' in next)
+ ) {
+ var type = next.type;
- // since key is captured we use const
- for (const key in next) {
- // let instead of var because referenced in a closure
- let value = next[key];
-
- // Up here because we want to do this for the initial value, too, even if it's undefined,
- // and this wouldn't be reached in case of undefined because of the equality check below
- if (is_option_element && key === 'value' && value == null) {
- // The