diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index e8213d3cf4a8..77140dc6903d 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -36,12 +36,7 @@ let todos = $state([
...modifying an individual todo's property will trigger updates to anything in your UI that depends on that specific property:
```js
-// @filename: ambient.d.ts
-declare global {
- const todos: Array<{ done: boolean, text: string }>
-}
-
-// @filename: index.js
+let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
todos[0].done = !todos[0].done;
```
@@ -64,6 +59,17 @@ todos.push({
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated.
+Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:
+
+```js
+let todos = [{ done: false, text: 'add more todos' }];
+// ---cut---
+let { done, text } = todos[0];
+
+// this will not affect the value of `done`
+todos[0].done = !todos[0].done;
+```
+
### Classes
You can also use `$state` in class fields (whether public or private):
@@ -85,7 +91,42 @@ class Todo {
}
```
-> [!NOTE] The compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields.
+> [!NOTE] The compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields. This means the properties are not enumerable.
+
+When calling methods in JavaScript, the value of [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) matters. This won't work, because `this` inside the `reset` method will be the `
` will result in `
hello
world
` for example (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
+- `
hello
world
` will result in `
hello
world
` (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
- `` will result in `` (the `
` is removed)
- `
cell
` will result in `
cell
` (a `` is auto-inserted)
diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md
index 775e0681c94f..481959ba3d5c 100644
--- a/documentation/docs/98-reference/.generated/compile-warnings.md
+++ b/documentation/docs/98-reference/.generated/compile-warnings.md
@@ -643,12 +643,12 @@ Svelte 5 components are no longer classes. Instantiate them using `mount` or `hy
### node_invalid_placement_ssr
```
-%thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning
+%message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
```
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
-- `
hello
world
` will result in `
hello
world
` for example (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
+- `
hello
world
` will result in `
hello
world
` (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
- `` will result in `` (the `
` is removed)
- `
cell
` will result in `
cell
` (a `` is auto-inserted)
@@ -726,12 +726,6 @@ Reactive declarations only exist at the top level of the instance script
Reassignments of module-level declarations will not cause reactive statements to update
```
-### reactive_declaration_non_reactive_property
-
-```
-Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
-```
-
### script_context_deprecated
```
diff --git a/documentation/docs/99-legacy/20-legacy-slots.md b/documentation/docs/99-legacy/20-legacy-slots.md
index 5189f2017db8..3474782e93ae 100644
--- a/documentation/docs/99-legacy/20-legacy-slots.md
+++ b/documentation/docs/99-legacy/20-legacy-slots.md
@@ -74,39 +74,45 @@ If no slotted content is provided, a component can define fallback content by pu
Slots can be rendered zero or more times and can pass values _back_ to the parent using props. The parent exposes the values to the slot template using the `let:` directive.
-The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `` is equivalent to ``.
-
```svelte
-
+
- {#each items as item}
+ {#each items as data}
-
+
+
{/each}
+```
-
-
-
{thing.text}
+```svelte
+
+
+
+
{processed.text}
```
+The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `` is equivalent to ``.
+
Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute.
```svelte
-
+
{#each items as item}
-
+
{/each}
+```
-
+```svelte
+
{item.text}
Copyright (c) 2019 Svelte Industries
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index a53738689fdf..b19cfb355b1e 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,77 @@
# svelte
+## 5.7.1
+
+### Patch Changes
+
+- fix: ensure bindings always take precedence over spreads ([#14575](https://github.com/sveltejs/svelte/pull/14575))
+
+## 5.7.0
+
+### Minor Changes
+
+- feat: add `createSubscriber` function for creating reactive values that depend on subscriptions ([#14422](https://github.com/sveltejs/svelte/pull/14422))
+
+- feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance ([#14422](https://github.com/sveltejs/svelte/pull/14422))
+
+### Patch Changes
+
+- fix: treat `undefined` and `null` the same for the initial input value ([#14562](https://github.com/sveltejs/svelte/pull/14562))
+
+## 5.6.2
+
+### Patch Changes
+
+- chore: make if blocks tree-shakable ([#14549](https://github.com/sveltejs/svelte/pull/14549))
+
+## 5.6.1
+
+### Patch Changes
+
+- fix: handle static form values in combination with default values ([#14555](https://github.com/sveltejs/svelte/pull/14555))
+
+## 5.6.0
+
+### Minor Changes
+
+- feat: support `defaultValue/defaultChecked` for inputs ([#14289](https://github.com/sveltejs/svelte/pull/14289))
+
+## 5.5.4
+
+### Patch Changes
+
+- fix: better error messages for invalid HTML trees ([#14445](https://github.com/sveltejs/svelte/pull/14445))
+
+- fix: remove spreaded event handlers when they become nullish ([#14546](https://github.com/sveltejs/svelte/pull/14546))
+
+- fix: respect the unidirectional nature of time ([#14541](https://github.com/sveltejs/svelte/pull/14541))
+
+## 5.5.3
+
+### Patch Changes
+
+- fix: don't try to add owners to non-`$state` class fields ([#14533](https://github.com/sveltejs/svelte/pull/14533))
+
+- fix: capture infinite_loop_guard in error boundary ([#14534](https://github.com/sveltejs/svelte/pull/14534))
+
+- fix: proxify values when assigning using `||=`, `&&=` and `??=` operators ([#14273](https://github.com/sveltejs/svelte/pull/14273))
+
+## 5.5.2
+
+### Patch Changes
+
+- fix: use correct reaction when lazily creating deriveds inside `SvelteDate` ([#14525](https://github.com/sveltejs/svelte/pull/14525))
+
+## 5.5.1
+
+### Patch Changes
+
+- fix: don't throw with nullish actions ([#13559](https://github.com/sveltejs/svelte/pull/13559))
+
+- fix: leave update expressions untransformed unless a transformer is provided ([#14507](https://github.com/sveltejs/svelte/pull/14507))
+
+- chore: turn reactive_declaration_non_reactive_property into a runtime warning ([#14192](https://github.com/sveltejs/svelte/pull/14192))
+
## 5.5.0
### Minor Changes
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 035fa49c31a7..8800b65172dc 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -1103,6 +1103,11 @@ export interface HTMLInputAttributes extends HTMLAttributes {
step?: number | string | undefined | null;
type?: HTMLInputTypeAttribute | undefined | null;
value?: any;
+ // needs both casing variants because language tools does lowercase names of non-shorthand attributes
+ defaultValue?: any;
+ defaultvalue?: any;
+ defaultChecked?: any;
+ defaultchecked?: any;
width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
@@ -1384,6 +1389,9 @@ export interface HTMLTextareaAttributes extends HTMLAttributes | undefined | null;
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index e9014207fd2b..019e83e7b120 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -1,3 +1,35 @@
+## assignment_value_stale
+
+> Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
+
+Given a case like this...
+
+```svelte
+
+
+add
+
items: {JSON.stringify(object.items)}
+```
+
+...the array being pushed to when the button is first clicked is the `[]` on the right-hand side of the assignment, but the resulting value of `object.array` is an empty state proxy. As a result, the pushed value will be discarded.
+
+You can fix this by separating it into two statements:
+
+```js
+let object = { array: [0] };
+// ---cut---
+function add() {
+ object.array ??= [];
+ object.array.push(object.array.length);
+}
+```
+
## binding_property_non_reactive
> `%binding%` is binding to a non-reactive property
@@ -54,6 +86,44 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+## reactive_declaration_non_reactive_property
+
+> A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
+
+In legacy mode, a `$:` [reactive statement](https://svelte.dev/docs/svelte/legacy-reactive-assignments) re-runs when the state it _references_ changes. This is determined at compile time, by analysing the code.
+
+In runes mode, effects and deriveds re-run when there are changes to the values that are read during the function's _execution_.
+
+Often, the result is the same — for example these can be considered equivalent:
+
+```js
+let a = 1, b = 2, sum = 3;
+// ---cut---
+$: sum = a + b;
+```
+
+```js
+let a = 1, b = 2;
+// ---cut---
+const sum = $derived(a + b);
+```
+
+In some cases — such as the one that triggered the above warning — they are _not_ the same:
+
+```js
+let a = 1, b = 2, sum = 3;
+// ---cut---
+const add = () => a + b;
+
+// the compiler can't 'see' that `sum` depends on `a` and `b`, but
+// they _would_ be read while executing the `$derived` version
+$: sum = add();
+```
+
+Similarly, reactive properties of [deep state](https://svelte.dev/docs/svelte/$state#Deep-state) are not visible to the compiler. As such, changes to these properties will cause effects and deriveds to re-run but will _not_ cause `$:` statements to re-run.
+
+When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
+
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md
index 613d11ae5165..9621a6457ba9 100644
--- a/packages/svelte/messages/compile-errors/template.md
+++ b/packages/svelte/messages/compile-errors/template.md
@@ -190,11 +190,11 @@
## node_invalid_placement
-> %thing% is invalid inside `<%parent%>`
+> %message%. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
-- `
hello
world
` will result in `
hello
world
` for example (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
+- `
hello
world
` will result in `
hello
world
` (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
- `` will result in `` (the `
` is removed)
- `
cell
` will result in `
cell
` (a `` is auto-inserted)
diff --git a/packages/svelte/messages/compile-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md
index 293f065ba768..2c891b4fc791 100644
--- a/packages/svelte/messages/compile-warnings/script.md
+++ b/packages/svelte/messages/compile-warnings/script.md
@@ -26,10 +26,6 @@
> Reassignments of module-level declarations will not cause reactive statements to update
-## reactive_declaration_non_reactive_property
-
-> Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
-
## state_referenced_locally
> State referenced in its own scope will never update. Did you mean to reference it inside a closure?
diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md
index bfa75ac7f02e..690681c172a3 100644
--- a/packages/svelte/messages/compile-warnings/template.md
+++ b/packages/svelte/messages/compile-warnings/template.md
@@ -40,11 +40,11 @@
## node_invalid_placement_ssr
-> %thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning
+> %message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
-- `
hello
world
` will result in `
hello
world
` for example (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
+- `
hello
world
` will result in `
hello
world
` (the `
` autoclosed the `
` because `
` cannot contain block-level elements)
- `` will result in `` (the `
` is removed)
- `
cell
` will result in `
cell
` (a `` is auto-inserted)
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index fe25b0bb3472..6a01e01ecb86 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.5.0",
+ "version": "5.7.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index a6e4549e2f0e..901ea1983ea7 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -1043,14 +1043,13 @@ export function mixed_event_handler_syntaxes(node, name) {
}
/**
- * %thing% is invalid inside `<%parent%>`
+ * %message%. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.
* @param {null | number | NodeLike} node
- * @param {string} thing
- * @param {string} parent
+ * @param {string} message
* @returns {never}
*/
-export function node_invalid_placement(node, thing, parent) {
- e(node, "node_invalid_placement", `${thing} is invalid inside \`<${parent}>\``);
+export function node_invalid_placement(node, message) {
+ e(node, "node_invalid_placement", `${message}. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.`);
}
/**
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js
index e0d2710a08f0..88fe4e6afaee 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js
@@ -12,8 +12,9 @@ export function ExpressionTag(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && context.state.parent_element) {
- if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
- e.node_invalid_placement(node, '`{expression}`', context.state.parent_element);
+ const message = is_tag_valid_with_parent('#text', context.state.parent_element);
+ if (message) {
+ e.node_invalid_placement(node, message);
}
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
index 1cc20c96dac8..6ea8f238e150 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
@@ -26,30 +26,5 @@ export function MemberExpression(node, context) {
context.state.analysis.needs_context = true;
}
- if (context.state.reactive_statement) {
- const left = object(node);
-
- if (left !== null) {
- const binding = context.state.scope.get(left.name);
-
- if (binding && binding.kind === 'normal') {
- const parent = /** @type {Node} */ (context.path.at(-1));
-
- if (
- binding.scope === context.state.analysis.module.scope ||
- binding.declaration_kind === 'import' ||
- (binding.initial &&
- binding.initial.type !== 'ArrayExpression' &&
- binding.initial.type !== 'ObjectExpression' &&
- binding.scope.function_depth <= 1)
- ) {
- if (parent.type !== 'MemberExpression' && parent.type !== 'CallExpression') {
- w.reactive_declaration_non_reactive_property(node);
- }
- }
- }
- }
- }
-
context.next();
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
index fa6ca0f6e970..7454ab810354 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
@@ -114,15 +114,12 @@ export function RegularElement(node, context) {
if (!past_parent) {
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
- if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
+ const message = is_tag_valid_with_parent(node.name, context.state.parent_element);
+ if (message) {
if (only_warn) {
- w.node_invalid_placement_ssr(
- node,
- `\`<${node.name}>\``,
- context.state.parent_element
- );
+ w.node_invalid_placement_ssr(node, message);
} else {
- e.node_invalid_placement(node, `\`<${node.name}>\``, context.state.parent_element);
+ e.node_invalid_placement(node, message);
}
}
@@ -131,11 +128,12 @@ export function RegularElement(node, context) {
} else if (ancestor.type === 'RegularElement') {
ancestors.push(ancestor.name);
- if (!is_tag_valid_with_ancestor(node.name, ancestors)) {
+ const message = is_tag_valid_with_ancestor(node.name, ancestors);
+ if (message) {
if (only_warn) {
- w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name);
+ w.node_invalid_placement_ssr(node, message);
} else {
- e.node_invalid_placement(node, `\`<${node.name}>\``, ancestor.name);
+ e.node_invalid_placement(node, message);
}
}
} else if (
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
index b60030f6389d..363a111b7dc6 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
@@ -12,8 +12,9 @@ export function Text(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
- if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
- e.node_invalid_placement(node, 'Text node', context.state.parent_element);
+ const message = is_tag_valid_with_parent('#text', context.state.parent_element);
+ if (message) {
+ e.node_invalid_placement(node, message);
}
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 80136159956a..47af9813e2a0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -32,7 +32,7 @@ export interface ClientTransformState extends TransformState {
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
- mutate?: (node: Identifier, mutation: AssignmentExpression) => Expression;
+ mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
update?: (node: UpdateExpression) => Expression;
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index ee909ede91bf..66ea2c4941a4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -1,8 +1,14 @@
-/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
+/** @import { Location } from 'locate-character' */
+/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */
+/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
-import { build_assignment_value } from '../../../../utils/ast.js';
-import { is_ignored } from '../../../../state.js';
+import {
+ build_assignment_value,
+ get_attribute_expression,
+ is_event_attribute
+} from '../../../../utils/ast.js';
+import { dev, filename, is_ignored, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
@@ -20,6 +26,24 @@ export function AssignmentExpression(node, context) {
: expression;
}
+/**
+ * Determines whether the value will be coerced on assignment (as with e.g. `+=`).
+ * If not, we may need to proxify the value, or warn that the value will not be
+ * proxified in time
+ * @param {AssignmentOperator} operator
+ */
+function is_non_coercive_operator(operator) {
+ return ['=', '||=', '&&=', '??='].includes(operator);
+}
+
+/** @type {Record} */
+const callees = {
+ '=': '$.assign',
+ '&&=': '$.assign_and',
+ '||=': '$.assign_or',
+ '??=': '$.assign_nullish'
+};
+
/**
* @param {AssignmentOperator} operator
* @param {Pattern} left
@@ -41,7 +65,11 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
- if (private_state.kind !== 'raw_state' && should_proxy(value, context.state.scope)) {
+ if (
+ private_state.kind === 'state' &&
+ is_non_coercive_operator(operator) &&
+ should_proxy(value, context.state.scope)
+ ) {
value = build_proxy_reassignment(value, b.member(b.this, private_state.id));
}
@@ -73,24 +101,28 @@ function build_assignment(operator, left, right, context) {
? context.state.transform[object.name]
: null;
+ const path = context.path.map((node) => node.type);
+
// reassignment
if (object === left && transform?.assign) {
+ // special case — if an element binding, we know it's a primitive
+
+ const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
+
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
- // special case — if an element binding, we know it's a primitive
- const path = context.path.map((node) => node.type);
- const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
-
if (
!is_primitive &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
+ binding.kind !== 'raw_state' &&
context.state.analysis.runes &&
- should_proxy(value, context.state.scope)
+ should_proxy(right, context.state.scope) &&
+ is_non_coercive_operator(operator)
) {
- value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object);
+ value = build_proxy_reassignment(value, object);
}
return transform.assign(object, value);
@@ -108,5 +140,57 @@ function build_assignment(operator, left, right, context) {
);
}
+ // in cases like `(object.items ??= []).push(value)`, we may need to warn
+ // if the value gets proxified, since the proxy _isn't_ the thing that
+ // will be pushed to. we do this by transforming it to something like
+ // `$.assign_nullish(object, 'items', [])`
+ let should_transform =
+ dev && path.at(-1) !== 'ExpressionStatement' && is_non_coercive_operator(operator);
+
+ // special case — ignore `onclick={() => (...)}`
+ if (
+ path.at(-1) === 'ArrowFunctionExpression' &&
+ (path.at(-2) === 'RegularElement' || path.at(-2) === 'SvelteElement')
+ ) {
+ const element = /** @type {AST.RegularElement} */ (context.path.at(-2));
+
+ const attribute = element.attributes.find((attribute) => {
+ if (attribute.type !== 'Attribute' || !is_event_attribute(attribute)) {
+ return false;
+ }
+
+ const expression = get_attribute_expression(attribute);
+
+ return expression === context.path.at(-1);
+ });
+
+ if (attribute) {
+ should_transform = false;
+ }
+ }
+
+ if (left.type === 'MemberExpression' && should_transform) {
+ const callee = callees[operator];
+
+ const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
+ const location = `${filename}:${loc.line}:${loc.column}`;
+
+ return /** @type {Expression} */ (
+ context.visit(
+ b.call(
+ callee,
+ /** @type {Expression} */ (left.object),
+ /** @type {Expression} */ (
+ left.computed
+ ? left.property
+ : b.literal(/** @type {Identifier} */ (left.property).name)
+ ),
+ right,
+ b.literal(location)
+ )
+ )
+ );
+ }
+
return null;
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index 11a524d33c55..5e842a82febf 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -184,17 +184,22 @@ export function ClassBody(node, context) {
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
- Array.from(public_state.keys()).map((name) =>
- b.stmt(
- b.call(
- '$.add_owner',
- b.call('$.get', b.member(b.this, b.private_id(name))),
- b.id('owner'),
- b.literal(false),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ Array.from(public_state)
+ // Only run ownership addition on $state fields.
+ // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
+ // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
+ .filter(([_, { kind }]) => kind === 'state')
+ .map(([name]) =>
+ b.stmt(
+ b.call(
+ '$.add_owner',
+ b.call('$.get', b.member(b.this, b.private_id(name))),
+ b.id('owner'),
+ b.literal(false),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
+ )
)
- )
- ),
+ ),
true
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
index d34f39f4c7b0..9f70981205a1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
@@ -214,7 +214,10 @@ export function EachBlock(node, context) {
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
- mutate: (_, mutation) => b.sequence([mutation, ...sequence])
+ mutate: (_, mutation) => {
+ uses_index = true;
+ return b.sequence([mutation, ...sequence]);
+ }
};
delete key_state.transform[node.context.name];
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
index 0aa6e5d24a53..d658f9eaf819 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
@@ -9,23 +9,44 @@ import * as b from '../../../../utils/builders.js';
*/
export function IfBlock(node, context) {
context.state.template.push('');
+ const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
+ const consequent_id = context.state.scope.generate('consequent');
+ statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));
+
+ let alternate_id;
+
+ if (node.alternate) {
+ const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
+ alternate_id = context.state.scope.generate('alternate');
+ statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate)));
+ }
+
+ /** @type {Expression[]} */
const args = [
context.state.node,
- b.thunk(/** @type {Expression} */ (context.visit(node.test))),
- b.arrow([b.id('$$anchor')], consequent)
+ b.arrow(
+ [b.id('$$render')],
+ b.block([
+ b.if(
+ /** @type {Expression} */ (context.visit(node.test)),
+ b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
+ alternate_id
+ ? b.stmt(
+ b.call(
+ b.id('$$render'),
+ b.id(alternate_id),
+ node.alternate ? b.literal(false) : undefined
+ )
+ )
+ : undefined
+ )
+ ])
+ )
];
- if (node.alternate || node.elseif) {
- args.push(
- node.alternate
- ? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.alternate)))
- : b.literal(null)
- );
- }
-
if (node.elseif) {
// We treat this...
//
@@ -51,5 +72,7 @@ export function IfBlock(node, context) {
args.push(b.literal(true));
}
- context.state.init.push(b.stmt(b.call('$.if', ...args)));
+ statements.push(b.stmt(b.call('$.if', ...args)));
+
+ context.state.init.push(b.block(statements));
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
index 87f56262a8ba..8ca6534457ec 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
@@ -1,6 +1,8 @@
+/** @import { Location } from 'locate-character' */
/** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
+import { dev, is_ignored, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { build_getter } from '../utils.js';
@@ -48,6 +50,11 @@ export function LabeledStatement(node, context) {
sequence.push(serialized);
}
+ const location =
+ dev && !is_ignored(node, 'reactive_declaration_non_reactive_property')
+ ? locator(/** @type {number} */ (node.start))
+ : undefined;
+
// these statements will be topologically ordered later
context.state.legacy_reactive_statements.set(
node,
@@ -55,7 +62,9 @@ export function LabeledStatement(node, context) {
b.call(
'$.legacy_pre_effect',
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
- b.thunk(b.block(body))
+ b.thunk(b.block(body)),
+ location && b.literal(location.line),
+ location && b.literal(location.column)
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
index c34a9b05c69e..29403ca6edef 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
@@ -77,13 +77,15 @@ export function Program(_, context) {
return b.call(
'$.store_mutate',
get_store(),
- b.assignment(
- mutation.operator,
- /** @type {MemberExpression} */ (
- replace(/** @type {MemberExpression} */ (mutation.left))
- ),
- mutation.right
- ),
+ mutation.type === 'AssignmentExpression'
+ ? b.assignment(
+ mutation.operator,
+ /** @type {MemberExpression} */ (
+ replace(/** @type {MemberExpression} */ (mutation.left))
+ ),
+ mutation.right
+ )
+ : b.update(mutation.operator, replace(mutation.argument), mutation.prefix),
untracked
);
},
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
index 85df92e8bfd0..3c0be589c363 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
@@ -172,20 +172,28 @@ export function RegularElement(node, context) {
}
}
- if (
- node.name === 'input' &&
- (has_spread ||
- bindings.has('value') ||
- bindings.has('checked') ||
- bindings.has('group') ||
- attributes.some(
- (attribute) =>
- attribute.type === 'Attribute' &&
- (attribute.name === 'value' || attribute.name === 'checked') &&
- !is_text_attribute(attribute)
- ))
- ) {
- context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
+ if (node.name === 'input') {
+ const has_value_attribute = attributes.some(
+ (attribute) =>
+ attribute.type === 'Attribute' &&
+ (attribute.name === 'value' || attribute.name === 'checked') &&
+ !is_text_attribute(attribute)
+ );
+ const has_default_value_attribute = attributes.some(
+ (attribute) =>
+ attribute.type === 'Attribute' &&
+ (attribute.name === 'defaultValue' || attribute.name === 'defaultChecked')
+ );
+ if (
+ !has_default_value_attribute &&
+ (has_spread ||
+ bindings.has('value') ||
+ bindings.has('checked') ||
+ bindings.has('group') ||
+ (!bindings.has('group') && has_value_attribute))
+ ) {
+ context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
+ }
}
if (node.name === 'textarea') {
@@ -283,7 +291,7 @@ export function RegularElement(node, context) {
const is = is_custom_element
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
- : build_element_attribute_update_assignment(node, node_id, attribute, context);
+ : build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
if (is) is_attributes_reactive = true;
}
}
@@ -511,10 +519,17 @@ function setup_select_synchronization(value_binding, context) {
* @param {AST.RegularElement} element
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
+ * @param {Array} attributes
* @param {ComponentContext} context
* @returns {boolean}
*/
-function build_element_attribute_update_assignment(element, node_id, attribute, context) {
+function build_element_attribute_update_assignment(
+ element,
+ node_id,
+ attribute,
+ attributes,
+ context
+) {
const state = context.state;
const name = get_attribute_name(element, attribute);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
@@ -555,6 +570,28 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
update = b.stmt(b.call('$.set_value', node_id, value));
} else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value));
+ } else if (name === 'selected') {
+ update = b.stmt(b.call('$.set_selected', node_id, value));
+ } else if (
+ // If we would just set the defaultValue property, it would override the value property,
+ // because it is set in the template which implicitly means it's also setting the default value,
+ // and if one updates the default value while the input is pristine it will also update the
+ // current value, which is not what we want, which is why we need to do some extra work.
+ name === 'defaultValue' &&
+ (attributes.some(
+ (attr) => attr.type === 'Attribute' && attr.name === 'value' && is_text_attribute(attr)
+ ) ||
+ (element.name === 'textarea' && element.fragment.nodes.length > 0))
+ ) {
+ update = b.stmt(b.call('$.set_default_value', node_id, value));
+ } else if (
+ // See defaultValue comment
+ name === 'defaultChecked' &&
+ attributes.some(
+ (attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
+ )
+ ) {
+ update = b.stmt(b.call('$.set_default_checked', node_id, value));
} else if (is_dom_property(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
index 91383a56793c..13c1b4bc51e1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
@@ -1,4 +1,4 @@
-/** @import { Expression, Node, Pattern, Statement, UpdateExpression } from 'estree' */
+/** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
@@ -34,34 +34,22 @@ export function UpdateExpression(node, context) {
}
const left = object(argument);
- if (left === null) return context.next();
+ const transformers = left && context.state.transform[left.name];
- if (left === argument) {
- const transform = context.state.transform;
- const update = transform[left.name]?.update;
-
- if (update && Object.hasOwn(transform, left.name)) {
- return update(node);
- }
+ if (left === argument && transformers?.update) {
+ // we don't need to worry about ownership_invalid_mutation here, because
+ // we're not mutating but reassigning
+ return transformers.update(node);
}
- const assignment = /** @type {Expression} */ (
- context.visit(
- b.assignment(
- node.operator === '++' ? '+=' : '-=',
- /** @type {Pattern} */ (argument),
- b.literal(1)
- )
- )
- );
+ let update = /** @type {Expression} */ (context.next());
- const parent = /** @type {Node} */ (context.path.at(-1));
- const is_standalone = parent.type === 'ExpressionStatement'; // TODO and possibly others, but not e.g. the `test` of a WhileStatement
-
- const update =
- node.prefix || is_standalone
- ? assignment
- : b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1));
+ if (left && transformers?.mutate) {
+ update = transformers.mutate(
+ left,
+ /** @type {AssignmentExpression | UpdateExpression} */ (update)
+ );
+ }
return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(update))
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
index f6d918ce4552..be9eb2d51669 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
@@ -20,7 +20,10 @@ export function UseDirective(node, context) {
context.state.node,
b.arrow(
params,
- b.call(/** @type {Expression} */ (context.visit(parse_directive_name(node.name))), ...params)
+ b.maybe_call(
+ /** @type {Expression} */ (context.visit(parse_directive_name(node.name))),
+ ...params
+ )
)
];
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index 8e1a53670708..aa7be93cb57e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -2,7 +2,7 @@
/** @import { AST, TemplateNode } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
-import { get_attribute_chunks } from '../../../../../utils/ast.js';
+import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { create_derived } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js';
@@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js';
export function build_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array} */
const props_and_spreads = [];
+ /** @type {Array<() => void>} */
+ const delayed_props = [];
/** @type {ExpressionStatement[]} */
const lets = [];
@@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context.
/**
* @param {Property} prop
+ * @param {boolean} [delay]
*/
- function push_prop(prop) {
- const current = props_and_spreads.at(-1);
- const current_is_props = Array.isArray(current);
- const props = current_is_props ? current : [];
- props.push(prop);
- if (!current_is_props) {
- props_and_spreads.push(props);
+ function push_prop(prop, delay = false) {
+ const do_push = () => {
+ const current = props_and_spreads.at(-1);
+ const current_is_props = Array.isArray(current);
+ const props = current_is_props ? current : [];
+ props.push(prop);
+ if (!current_is_props) {
+ props_and_spreads.push(props);
+ }
+ };
+
+ if (delay) {
+ delayed_props.push(do_push);
+ } else {
+ do_push();
}
}
@@ -176,38 +187,53 @@ export function build_component(node, component_name, context, anchor = context.
bind_this = attribute.expression;
} else {
if (dev) {
- binding_initializers.push(
- b.stmt(
- b.call(
- b.id('$.add_owner_effect'),
- b.thunk(expression),
- b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ const left = object(attribute.expression);
+ let binding;
+ if (left?.type === 'Identifier') {
+ binding = context.state.scope.get(left.name);
+ }
+ // Only run ownership addition on $state fields.
+ // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
+ // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
+ if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') {
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ b.id('$.add_owner_effect'),
+ b.thunk(expression),
+ b.id(component_name),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
+ )
)
- )
- );
+ );
+ }
}
const is_store_sub =
attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
+ // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
if (is_store_sub) {
push_prop(
- b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
+ b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
+ true
);
} else {
- push_prop(b.get(attribute.name, [b.return(expression)]));
+ push_prop(b.get(attribute.name, [b.return(expression)]), true);
}
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
- b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
+ b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
+ true
);
}
}
}
+ delayed_props.forEach((fn) => fn());
+
if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index 79df3cdd04c6..7cabfb06c527 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js';
export function build_inline_component(node, expression, context) {
/** @type {Array} */
const props_and_spreads = [];
+ /** @type {Array<() => void>} */
+ const delayed_props = [];
/** @type {Property[]} */
const custom_css_props = [];
@@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) {
/**
* @param {Property} prop
+ * @param {boolean} [delay]
*/
- function push_prop(prop) {
- const current = props_and_spreads.at(-1);
- const current_is_props = Array.isArray(current);
- const props = current_is_props ? current : [];
- props.push(prop);
- if (!current_is_props) {
- props_and_spreads.push(props);
+ function push_prop(prop, delay = false) {
+ const do_push = () => {
+ const current = props_and_spreads.at(-1);
+ const current_is_props = Array.isArray(current);
+ const props = current_is_props ? current : [];
+ props.push(prop);
+ if (!current_is_props) {
+ props_and_spreads.push(props);
+ }
+ };
+
+ if (delay) {
+ delayed_props.push(do_push);
+ } else {
+ do_push();
}
}
@@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
- // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
+ // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
- ])
+ ]),
+ true
);
push_prop(
b.set(attribute.name, [
@@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) {
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
- ])
+ ]),
+ true
);
}
}
+ delayed_props.forEach((fn) => fn());
+
/** @type {Statement[]} */
const snippet_declarations = [];
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
index 2ab5d9b9fdfa..434447727b33 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
@@ -82,7 +82,8 @@ export function build_element_attributes(node, context) {
) {
events_to_capture.add(attribute.name);
}
- } else {
+ // the defaultValue/defaultChecked properties don't exist as attributes
+ } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
class_index = attributes.length;
} else if (attribute.name === 'style') {
diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js
index bd2895623050..e193ad0f1109 100644
--- a/packages/svelte/src/compiler/warnings.js
+++ b/packages/svelte/src/compiler/warnings.js
@@ -102,7 +102,6 @@ export const codes = [
"perf_avoid_nested_class",
"reactive_declaration_invalid_placement",
"reactive_declaration_module_script_dependency",
- "reactive_declaration_non_reactive_property",
"state_referenced_locally",
"store_rune_conflict",
"css_unused_selector",
@@ -641,14 +640,6 @@ export function reactive_declaration_module_script_dependency(node) {
w(node, "reactive_declaration_module_script_dependency", "Reassignments of module-level declarations will not cause reactive statements to update");
}
-/**
- * Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
- * @param {null | NodeLike} node
- */
-export function reactive_declaration_non_reactive_property(node) {
- w(node, "reactive_declaration_non_reactive_property", "Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update");
-}
-
/**
* State referenced in its own scope will never update. Did you mean to reference it inside a closure?
* @param {null | NodeLike} node
@@ -763,13 +754,12 @@ export function event_directive_deprecated(node, name) {
}
/**
- * %thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning
+ * %message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
* @param {null | NodeLike} node
- * @param {string} thing
- * @param {string} parent
+ * @param {string} message
*/
-export function node_invalid_placement_ssr(node, thing, parent) {
- w(node, "node_invalid_placement_ssr", `${thing} is invalid inside \`<${parent}>\`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a \`hydration_mismatch\` warning`);
+export function node_invalid_placement_ssr(node, message) {
+ w(node, "node_invalid_placement_ssr", `${message}. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a \`hydration_mismatch\` warning`);
}
/**
diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js
index 03fddc5ebd28..f8a7143b2047 100644
--- a/packages/svelte/src/constants.js
+++ b/packages/svelte/src/constants.js
@@ -44,7 +44,8 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'hydration_attribute_changed',
'hydration_html_changed',
'ownership_invalid_binding',
- 'ownership_invalid_mutation'
+ 'ownership_invalid_mutation',
+ 'reactive_declaration_non_reactive_property'
]);
/**
diff --git a/packages/svelte/src/html-tree-validation.js b/packages/svelte/src/html-tree-validation.js
index 0ebf45e166c2..98e74b638f1e 100644
--- a/packages/svelte/src/html-tree-validation.js
+++ b/packages/svelte/src/html-tree-validation.js
@@ -135,59 +135,85 @@ const disallowed_children = {
};
/**
- * Returns false if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result
+ * Returns an error message if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration.
- * @param {string} tag
+ * @param {string} child_tag
* @param {string[]} ancestors All nodes starting with the parent, up until the ancestor, which means two entries minimum
- * @returns {boolean}
+ * @param {string} [child_loc]
+ * @param {string} [ancestor_loc]
+ * @returns {string | null}
*/
-export function is_tag_valid_with_ancestor(tag, ancestors) {
- if (tag.includes('-')) return true; // custom elements can be anything
+export function is_tag_valid_with_ancestor(child_tag, ancestors, child_loc, ancestor_loc) {
+ if (child_tag.includes('-')) return null; // custom elements can be anything
- const target = ancestors[ancestors.length - 1];
- const disallowed = disallowed_children[target];
- if (!disallowed) return true;
+ const ancestor_tag = ancestors[ancestors.length - 1];
+ const disallowed = disallowed_children[ancestor_tag];
+ if (!disallowed) return null;
if ('reset_by' in disallowed && disallowed.reset_by) {
for (let i = ancestors.length - 2; i >= 0; i--) {
const ancestor = ancestors[i];
- if (ancestor.includes('-')) return true; // custom elements can be anything
+ if (ancestor.includes('-')) return null; // custom elements can be anything
// A reset means that forbidden descendants are allowed again
if (disallowed.reset_by.includes(ancestors[i])) {
- return true;
+ return null;
}
}
}
- return 'descendant' in disallowed ? !disallowed.descendant.includes(tag) : true;
+ if ('descendant' in disallowed && disallowed.descendant.includes(child_tag)) {
+ const child = child_loc ? `\`<${child_tag}>\` (${child_loc})` : `\`<${child_tag}>\``;
+ const ancestor = ancestor_loc
+ ? `\`<${ancestor_tag}>\` (${ancestor_loc})`
+ : `\`<${ancestor_tag}>\``;
+
+ return `${child} cannot be a descendant of ${ancestor}`;
+ }
+
+ return null;
}
/**
- * Returns false if the tag is not allowed inside the parent tag such that it will result
+ * Returns an error message if the tag is not allowed inside the parent tag such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration.
- * @param {string} tag
+ * @param {string} child_tag
* @param {string} parent_tag
- * @returns {boolean}
+ * @param {string} [child_loc]
+ * @param {string} [parent_loc]
+ * @returns {string | null}
*/
-export function is_tag_valid_with_parent(tag, parent_tag) {
- if (tag.includes('-') || parent_tag?.includes('-')) return true; // custom elements can be anything
+export function is_tag_valid_with_parent(child_tag, parent_tag, child_loc, parent_loc) {
+ if (child_tag.includes('-') || parent_tag?.includes('-')) return null; // custom elements can be anything
const disallowed = disallowed_children[parent_tag];
+ const child = child_loc ? `\`<${child_tag}>\` (${child_loc})` : `\`<${child_tag}>\``;
+ const parent = parent_loc ? `\`<${parent_tag}>\` (${parent_loc})` : `\`<${parent_tag}>\``;
+
if (disallowed) {
- if ('direct' in disallowed && disallowed.direct.includes(tag)) {
- return false;
+ if ('direct' in disallowed && disallowed.direct.includes(child_tag)) {
+ return `${child} cannot be a direct child of ${parent}`;
}
- if ('descendant' in disallowed && disallowed.descendant.includes(tag)) {
- return false;
+
+ if ('descendant' in disallowed && disallowed.descendant.includes(child_tag)) {
+ return `${child} cannot be a child of ${parent}`;
}
+
if ('only' in disallowed && disallowed.only) {
- return disallowed.only.includes(tag);
+ if (disallowed.only.includes(child_tag)) {
+ return null;
+ } else {
+ return `${child} cannot be a child of ${parent}. \`<${parent_tag}>\` only allows these children: ${disallowed.only.map((d) => `\`<${d}>\``).join(', ')}`;
+ }
}
}
- switch (tag) {
+ // These tags are only valid with a few parents that have special child
+ // parsing rules - if we're down here, then none of those matched and
+ // so we allow it only if we don't know what the parent is, as all other
+ // cases are invalid (and we only get into this function if we know the parent).
+ switch (child_tag) {
case 'body':
case 'caption':
case 'col':
@@ -196,18 +222,17 @@ export function is_tag_valid_with_parent(tag, parent_tag) {
case 'frame':
case 'head':
case 'html':
+ return `${child} cannot be a child of ${parent}`;
+ case 'thead':
case 'tbody':
- case 'td':
case 'tfoot':
+ return `${child} must be the child of a \`
\`, not a ${parent}`;
+ case 'td':
case 'th':
- case 'thead':
+ return `${child} must be the child of a \`
\`, not a ${parent}`;
case 'tr':
- // These tags are only valid with a few parents that have special child
- // parsing rules - if we're down here, then none of those matched and
- // so we allow it only if we don't know what the parent is, as all other
- // cases are invalid (and we only get into this function if we know the parent).
- return false;
+ return `\`
\` must be the child of a \`\`, \`
\`, or \`\`, not a ${parent}`;
}
- return true;
+ return null;
}
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 72811c8f17c9..587d76623331 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -83,7 +83,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* }>();
* ```
*
- * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events
+ * @deprecated Use callback props and/or the `$host()` rune instead — see [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events)
* @template {Record} [EventMap = any]
* @returns {EventDispatcher}
*/
@@ -122,7 +122,7 @@ export function createEventDispatcher() {
*
* In runes mode use `$effect.pre` instead.
*
- * @deprecated Use `$effect.pre` instead — see https://svelte.dev/docs/svelte/$effect#$effect.pre
+ * @deprecated Use [`$effect.pre`](https://svelte.dev/docs/svelte/$effect#$effect.pre) instead
* @param {() => void} fn
* @returns {void}
*/
@@ -145,7 +145,7 @@ export function beforeUpdate(fn) {
*
* In runes mode use `$effect` instead.
*
- * @deprecated Use `$effect` instead — see https://svelte.dev/docs/svelte/$effect
+ * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* @param {() => void} fn
* @returns {void}
*/
diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts
index b8ba8b6f0a75..e157ce76e2f3 100644
--- a/packages/svelte/src/index.d.ts
+++ b/packages/svelte/src/index.d.ts
@@ -53,7 +53,7 @@ export class SvelteComponent<
/**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
* is a stop-gap solution. Migrate towards using `mount` instead. See
- * https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more info.
+ * [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info.
*/
constructor(options: ComponentConstructorOptions>);
/**
@@ -83,14 +83,14 @@ export class SvelteComponent<
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
- * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes
+ * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info.
*/
$destroy(): void;
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
- * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes
+ * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info.
*/
$on>(
@@ -100,7 +100,7 @@ export class SvelteComponent<
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
- * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes
+ * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info.
*/
$set(props: Partial): void;
@@ -153,13 +153,13 @@ export interface Component<
): {
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
- * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes
+ * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info.
*/
$on?(type: string, callback: (e: any) => void): () => void;
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
- * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes
+ * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info.
*/
$set?(props: Partial): void;
diff --git a/packages/svelte/src/internal/client/dev/assign.js b/packages/svelte/src/internal/client/dev/assign.js
new file mode 100644
index 000000000000..cf8c31a941dd
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/assign.js
@@ -0,0 +1,57 @@
+import * as w from '../warnings.js';
+import { sanitize_location } from './location.js';
+
+/**
+ *
+ * @param {any} a
+ * @param {any} b
+ * @param {string} property
+ * @param {string} location
+ */
+function compare(a, b, property, location) {
+ if (a !== b) {
+ w.assignment_value_stale(property, /** @type {string} */ (sanitize_location(location)));
+ }
+
+ return a;
+}
+
+/**
+ * @param {any} object
+ * @param {string} property
+ * @param {any} value
+ * @param {string} location
+ */
+export function assign(object, property, value, location) {
+ return compare((object[property] = value), object[property], property, location);
+}
+
+/**
+ * @param {any} object
+ * @param {string} property
+ * @param {any} value
+ * @param {string} location
+ */
+export function assign_and(object, property, value, location) {
+ return compare((object[property] &&= value), object[property], property, location);
+}
+
+/**
+ * @param {any} object
+ * @param {string} property
+ * @param {any} value
+ * @param {string} location
+ */
+export function assign_or(object, property, value, location) {
+ return compare((object[property] ||= value), object[property], property, location);
+}
+
+/**
+ * @param {any} object
+ * @param {string} property
+ * @param {any} value
+ * @param {string} location
+ */
+export function assign_nullish(object, property, value, location) {
+ return compare((object[property] ??= value), object[property], property, location);
+}
diff --git a/packages/svelte/src/internal/client/dev/location.js b/packages/svelte/src/internal/client/dev/location.js
new file mode 100644
index 000000000000..b2e16c371e66
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/location.js
@@ -0,0 +1,25 @@
+import { DEV } from 'esm-env';
+import { FILENAME } from '../../../constants.js';
+import { dev_current_component_function } from '../runtime.js';
+
+/**
+ *
+ * @param {number} [line]
+ * @param {number} [column]
+ */
+export function get_location(line, column) {
+ if (!DEV || line === undefined) return undefined;
+
+ var filename = dev_current_component_function?.[FILENAME];
+ var location = filename && `${filename}:${line}:${column}`;
+
+ return sanitize_location(location);
+}
+
+/**
+ * Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
+ * @param {string | undefined} location
+ */
+export function sanitize_location(location) {
+ return location?.replace(/\//g, '/\u200b');
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index aa13336b296b..2f815b454e05 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -9,6 +9,7 @@ import { hash } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js';
+import { sanitize_location } from '../../dev/location.js';
/**
* @param {Element} element
@@ -28,9 +29,7 @@ function check_hash(element, server_hash, value) {
location = `in ${dev_current_component_function[FILENAME]}`;
}
- w.hydration_html_changed(
- location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
- );
+ w.hydration_html_changed(sanitize_location(location));
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 4d8e9412d3e2..6a880f28bc98 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -13,13 +13,11 @@ import { HYDRATION_START_ELSE } from '../../../../constants.js';
/**
* @param {TemplateNode} node
- * @param {() => boolean} get_condition
- * @param {(anchor: Node) => void} consequent_fn
- * @param {null | ((anchor: Node) => void)} [alternate_fn]
+ * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
-export function if_block(node, get_condition, consequent_fn, alternate_fn = null, elseif = false) {
+export function if_block(node, fn, elseif = false) {
if (hydrating) {
hydrate_next();
}
@@ -37,8 +35,18 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
var flags = elseif ? EFFECT_TRANSPARENT : 0;
- block(() => {
- if (condition === (condition = !!get_condition())) return;
+ var has_branch = false;
+
+ const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
+ has_branch = true;
+ update_branch(flag, fn);
+ };
+
+ const update_branch = (
+ /** @type {boolean | null} */ new_condition,
+ /** @type {null | ((anchor: Node) => void)} */ fn
+ ) => {
+ if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
@@ -60,8 +68,8 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
if (condition) {
if (consequent_effect) {
resume_effect(consequent_effect);
- } else {
- consequent_effect = branch(() => consequent_fn(anchor));
+ } else if (fn) {
+ consequent_effect = branch(() => fn(anchor));
}
if (alternate_effect) {
@@ -72,8 +80,8 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
} else {
if (alternate_effect) {
resume_effect(alternate_effect);
- } else if (alternate_fn) {
- alternate_effect = branch(() => alternate_fn(anchor));
+ } else if (fn) {
+ alternate_effect = branch(() => fn(anchor));
}
if (consequent_effect) {
@@ -87,6 +95,14 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
// continue in hydration mode
set_hydrating(true);
}
+ };
+
+ block(() => {
+ has_branch = false;
+ fn(set_branch);
+ if (!has_branch) {
+ update_branch(null, null);
+ }
}, flags);
if (hydrating) {
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index d927af543ff2..0276069eee49 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -60,13 +60,19 @@ export function remove_input_defaults(input) {
export function set_value(element, value) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
+
if (
- attributes.value === (attributes.value = value) ||
+ attributes.value ===
+ (attributes.value =
+ // treat null and undefined the same for the initial value
+ value ?? undefined) ||
// @ts-expect-error
// `progress` elements always need their value set when its `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
- )
+ ) {
return;
+ }
+
// @ts-expect-error
element.value = value;
}
@@ -79,11 +85,60 @@ export function set_checked(element, checked) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
- if (attributes.checked === (attributes.checked = checked)) return;
+ if (
+ attributes.checked ===
+ (attributes.checked =
+ // treat null and undefined the same for the initial value
+ checked ?? undefined)
+ ) {
+ return;
+ }
+
// @ts-expect-error
element.checked = checked;
}
+/**
+ * Sets the `selected` attribute on an `option` element.
+ * Not set through the property because that doesn't reflect to the DOM,
+ * which means it wouldn't be taken into account when a form is reset.
+ * @param {HTMLOptionElement} element
+ * @param {boolean} selected
+ */
+export function set_selected(element, selected) {
+ if (selected) {
+ // The selected option could've changed via user selection, and
+ // setting the value without this check would set it back.
+ if (!element.hasAttribute('selected')) {
+ element.setAttribute('selected', '');
+ }
+ } else {
+ element.removeAttribute('selected');
+ }
+}
+
+/**
+ * Applies the default checked property without influencing the current checked property.
+ * @param {HTMLInputElement} element
+ * @param {boolean} checked
+ */
+export function set_default_checked(element, checked) {
+ const existing_value = element.checked;
+ element.defaultChecked = checked;
+ element.checked = existing_value;
+}
+
+/**
+ * Applies the default value property without influencing the current value property.
+ * @param {HTMLInputElement | HTMLTextAreaElement} element
+ * @param {string} value
+ */
+export function set_default_value(element, value) {
+ const existing_value = element.value;
+ element.defaultValue = value;
+ element.value = existing_value;
+}
+
/**
* @param {Element} element
* @param {string} attribute
@@ -281,6 +336,9 @@ export function set_attributes(
element[`__${event_name}`] = value;
delegate([event_name]);
}
+ } else if (delegated) {
+ // @ts-ignore
+ element[`__${event_name}`] = undefined;
}
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
index aec6f815a012..810dcb08629d 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
@@ -1,11 +1,11 @@
import { DEV } from 'esm-env';
import { render_effect, teardown } from '../../../reactivity/effects.js';
-import { listen_to_event_and_reset_event, without_reactive_context } from './shared.js';
+import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
-import { is_runes } from '../../../runtime.js';
+import { is_runes, untrack } from '../../../runtime.js';
/**
* @param {HTMLInputElement} input
@@ -16,24 +16,36 @@ import { is_runes } from '../../../runtime.js';
export function bind_value(input, get, set = get) {
var runes = is_runes();
- listen_to_event_and_reset_event(input, 'input', () => {
+ listen_to_event_and_reset_event(input, 'input', (is_reset) => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
}
- /** @type {unknown} */
- var value = is_numberlike_input(input) ? to_number(input.value) : input.value;
+ /** @type {any} */
+ var value = is_reset ? input.defaultValue : input.value;
+ value = is_numberlike_input(input) ? to_number(value) : value;
set(value);
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) {
- // @ts-expect-error the value is coerced on assignment
+ // the value is coerced on assignment
input.value = value ?? '';
}
});
+ if (
+ // If we are hydrating and the value has since changed,
+ // then use the updated value from the input instead.
+ (hydrating && input.defaultValue !== input.value) ||
+ // If defaultValue is set, then value == defaultValue
+ // TODO Svelte 6: remove input.value check and set to empty string?
+ (untrack(get) == null && input.value)
+ ) {
+ set(is_numberlike_input(input) ? to_number(input.value) : input.value);
+ }
+
render_effect(() => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
@@ -42,13 +54,6 @@ export function bind_value(input, get, set = get) {
var value = get();
- // If we are hydrating and the value has since changed, then use the update value
- // from the input instead.
- if (hydrating && input.defaultValue !== input.value) {
- set(is_numberlike_input(input) ? to_number(input.value) : input.value);
- return;
- }
-
if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
return;
@@ -175,13 +180,19 @@ export function bind_group(inputs, group_index, input, get, set = get) {
* @returns {void}
*/
export function bind_checked(input, get, set = get) {
- listen_to_event_and_reset_event(input, 'change', () => {
- var value = input.checked;
+ listen_to_event_and_reset_event(input, 'change', (is_reset) => {
+ var value = is_reset ? input.defaultChecked : input.checked;
set(value);
});
- if (get() == undefined) {
- set(false);
+ if (
+ // If we are hydrating and the value has since changed,
+ // then use the update value from the input instead.
+ (hydrating && input.defaultChecked !== input.checked) ||
+ // If defaultChecked is set, then checked == defaultChecked
+ untrack(get) == null
+ ) {
+ set(input.checked);
}
render_effect(() => {
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
index 4d3dbff81249..32cd160de21e 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
@@ -80,15 +80,19 @@ export function init_select(select, get_value) {
export function bind_select_value(select, get, set = get) {
var mounting = true;
- listen_to_event_and_reset_event(select, 'change', () => {
+ listen_to_event_and_reset_event(select, 'change', (is_reset) => {
+ var query = is_reset ? '[selected]' : ':checked';
/** @type {unknown} */
var value;
if (select.multiple) {
- value = [].map.call(select.querySelectorAll(':checked'), get_option_value);
+ value = [].map.call(select.querySelectorAll(query), get_option_value);
} else {
/** @type {HTMLOptionElement | null} */
- var selected_option = select.querySelector(':checked');
+ var selected_option =
+ select.querySelector(query) ??
+ // will fall back to first non-disabled option if no option is selected
+ select.querySelector('option:not([disabled])');
value = selected_option && get_option_value(selected_option);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js
index 832b7f45e5d2..aa083776a5bc 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js
@@ -53,8 +53,8 @@ export function without_reactive_context(fn) {
* to notify all bindings when the form is reset
* @param {HTMLElement} element
* @param {string} event
- * @param {() => void} handler
- * @param {() => void} [on_reset]
+ * @param {(is_reset?: true) => void} handler
+ * @param {(is_reset?: true) => void} [on_reset]
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler));
@@ -65,11 +65,11 @@ export function listen_to_event_and_reset_event(element, event, handler, on_rese
// @ts-expect-error
element.__on_r = () => {
prev();
- on_reset();
+ on_reset(true);
};
} else {
// @ts-expect-error
- element.__on_r = on_reset;
+ element.__on_r = () => on_reset(true);
}
add_form_reset_listener();
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index e8cbefb090c0..b706e52a5378 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
+export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
@@ -33,7 +34,10 @@ export {
set_xlink_attribute,
handle_lazy_img,
set_value,
- set_checked
+ set_checked,
+ set_selected,
+ set_default_checked,
+ set_default_value
} from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
diff --git a/packages/svelte/src/internal/client/loop.js b/packages/svelte/src/internal/client/loop.js
index d1c73e344fa3..9c1e972800fd 100644
--- a/packages/svelte/src/internal/client/loop.js
+++ b/packages/svelte/src/internal/client/loop.js
@@ -4,10 +4,13 @@ import { raf } from './timing.js';
// TODO move this into timing.js where it probably belongs
/**
- * @param {number} now
* @returns {void}
*/
-function run_tasks(now) {
+function run_tasks() {
+ // use `raf.now()` instead of the `requestAnimationFrame` callback argument, because
+ // otherwise things can get wonky https://github.com/sveltejs/svelte/pull/14541
+ const now = raf.now();
+
raf.tasks.forEach((task) => {
if (!task.c(now)) {
raf.tasks.delete(task);
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 69b006fc0f9a..912aab37b6b3 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -16,7 +16,8 @@ import {
set_is_flushing_effect,
set_signal_status,
untrack,
- skip_reaction
+ skip_reaction,
+ capture_signals
} from '../runtime.js';
import {
DIRTY,
@@ -39,10 +40,13 @@ import {
} from '../constants.js';
import { set } from './sources.js';
import * as e from '../errors.js';
+import * as w from '../warnings.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { destroy_derived } from './deriveds.js';
+import { FILENAME } from '../../../constants.js';
+import { get_location } from '../dev/location.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -261,14 +265,21 @@ export function effect(fn) {
* Internal representation of `$: ..`
* @param {() => any} deps
* @param {() => void | (() => void)} fn
+ * @param {number} [line]
+ * @param {number} [column]
*/
-export function legacy_pre_effect(deps, fn) {
+export function legacy_pre_effect(deps, fn, line, column) {
var context = /** @type {ComponentContextLegacy} */ (component_context);
/** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l.r1.push(token);
+ if (DEV && line !== undefined) {
+ var location = get_location(line, column);
+ var explicit_deps = capture_signals(deps);
+ }
+
token.effect = render_effect(() => {
deps();
@@ -278,7 +289,18 @@ export function legacy_pre_effect(deps, fn) {
token.ran = true;
set(context.l.r2, true);
- untrack(fn);
+
+ if (DEV && location) {
+ var implicit_deps = capture_signals(() => untrack(fn));
+
+ for (var signal of implicit_deps) {
+ if (!explicit_deps.has(signal)) {
+ w.reactive_declaration_non_reactive_property(/** @type {string} */ (location));
+ }
+ }
+ } else {
+ untrack(fn);
+ }
});
}
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 2779898d6ffe..4928419d16af 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -48,6 +48,9 @@ let scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
+/** @type {Effect | null} */
+let last_scheduled_effect = null;
+
export let is_flushing_effect = false;
export let is_destroying_effect = false;
@@ -532,27 +535,47 @@ export function update_effect(effect) {
}
}
+function log_effect_stack() {
+ // eslint-disable-next-line no-console
+ console.error(
+ 'Last ten effects were: ',
+ dev_effect_stack.slice(-10).map((d) => d.fn)
+ );
+ dev_effect_stack = [];
+}
+
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
- if (DEV) {
- try {
- e.effect_update_depth_exceeded();
- } catch (error) {
+ try {
+ e.effect_update_depth_exceeded();
+ } catch (error) {
+ if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
- // eslint-disable-next-line no-console
- console.error(
- 'Last ten effects were: ',
- dev_effect_stack.slice(-10).map((d) => d.fn)
- );
- dev_effect_stack = [];
+ }
+ // Try and handle the error so it can be caught at a boundary, that's
+ // if there's an effect available from when it was last scheduled
+ if (last_scheduled_effect !== null) {
+ if (DEV) {
+ try {
+ handle_error(error, last_scheduled_effect, null, null);
+ } catch (e) {
+ // Only log the effect stack if the error is re-thrown
+ log_effect_stack();
+ throw e;
+ }
+ } else {
+ handle_error(error, last_scheduled_effect, null, null);
+ }
+ } else {
+ if (DEV) {
+ log_effect_stack();
+ }
throw error;
}
- } else {
- e.effect_update_depth_exceeded();
}
}
flush_count++;
@@ -637,8 +660,10 @@ function process_deferred() {
const previous_queued_root_effects = queued_root_effects;
queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
+
if (!is_micro_task_queued) {
flush_count = 0;
+ last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
@@ -657,6 +682,8 @@ export function schedule_effect(signal) {
}
}
+ last_scheduled_effect = signal;
+
var effect = signal;
while (effect.parent !== null) {
@@ -776,6 +803,7 @@ export function flush_sync(fn) {
}
flush_count = 0;
+ last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
@@ -894,15 +922,17 @@ export function safe_get(signal) {
}
/**
- * Invokes a function and captures all signals that are read during the invocation,
- * then invalidates them.
- * @param {() => any} fn
+ * Capture an array of all the signals that are read when `fn` is called
+ * @template T
+ * @param {() => T} fn
*/
-export function invalidate_inner_signals(fn) {
+export function capture_signals(fn) {
var previous_captured_signals = captured_signals;
captured_signals = new Set();
+
var captured = captured_signals;
var signal;
+
try {
untrack(fn);
if (previous_captured_signals !== null) {
@@ -913,7 +943,19 @@ export function invalidate_inner_signals(fn) {
} finally {
captured_signals = previous_captured_signals;
}
- for (signal of captured) {
+
+ return captured;
+}
+
+/**
+ * Invokes a function and captures all signals that are read during the invocation,
+ * then invalidates them.
+ * @param {() => any} fn
+ */
+export function invalidate_inner_signals(fn) {
+ var captured = capture_signals(() => untrack(fn));
+
+ for (var signal of captured) {
// Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {Derived} */ (signal).deps || []) {
diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 047831371912..2f28d3e9e3a4 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -5,6 +5,20 @@ import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
+/**
+ * Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
+ * @param {string} property
+ * @param {string} location
+ */
+export function assignment_value_stale(property, location) {
+ if (DEV) {
+ console.warn(`%c[svelte] assignment_value_stale\n%cAssignment to \`${property}\` property (${location}) will evaluate to the right-hand side, not the value of \`${property}\` following the assignment. This may result in unexpected behaviour.`, bold, normal);
+ } else {
+ // TODO print a link to the documentation
+ console.warn("assignment_value_stale");
+ }
+}
+
/**
* `%binding%` (%location%) is binding to a non-reactive property
* @param {string} binding
@@ -153,6 +167,19 @@ export function ownership_invalid_mutation(component, owner) {
}
}
+/**
+ * A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
+ * @param {string} location
+ */
+export function reactive_declaration_non_reactive_property(location) {
+ if (DEV) {
+ console.warn(`%c[svelte] reactive_declaration_non_reactive_property\n%cA \`$:\` statement (${location}) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode`, bold, normal);
+ } else {
+ // TODO print a link to the documentation
+ console.warn("reactive_declaration_non_reactive_property");
+ }
+}
+
/**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index 145b37479b3f..ecf4e67429ac 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -34,12 +34,11 @@ function stringify(element) {
/**
* @param {Payload} payload
- * @param {Element} parent
- * @param {Element} child
+ * @param {string} message
*/
-function print_error(payload, parent, child) {
- var message =
- `node_invalid_placement_ssr: ${stringify(parent)} cannot contain ${stringify(child)}\n\n` +
+function print_error(payload, message) {
+ message =
+ `node_invalid_placement_ssr: ${message}\n\n` +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
if ((seen ??= new Set()).has(message)) return;
@@ -72,15 +71,23 @@ export function push_element(payload, tag, line, column) {
var ancestor = parent.parent;
var ancestors = [parent.tag];
- if (!is_tag_valid_with_parent(tag, parent.tag)) {
- print_error(payload, parent, child);
- }
+ const child_loc = filename ? `${filename}:${line}:${column}` : undefined;
+ const parent_loc = parent.filename
+ ? `${parent.filename}:${parent.line}:${parent.column}`
+ : undefined;
+
+ const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
+ if (message) print_error(payload, message);
while (ancestor != null) {
ancestors.push(ancestor.tag);
- if (!is_tag_valid_with_ancestor(tag, ancestors)) {
- print_error(payload, ancestor, child);
- }
+ const ancestor_loc = ancestor.filename
+ ? `${ancestor.filename}:${ancestor.line}:${ancestor.column}`
+ : undefined;
+
+ const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
+ if (message) print_error(payload, message);
+
ancestor = ancestor.parent;
}
}
diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js
index 10f52502d372..f4262a565024 100644
--- a/packages/svelte/src/motion/index.js
+++ b/packages/svelte/src/motion/index.js
@@ -1,2 +1,32 @@
+import { MediaQuery } from 'svelte/reactivity';
+
export * from './spring.js';
export * from './tweened.js';
+
+/**
+ * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
+ *
+ * ```svelte
+ *
+ *
+ * visible = !visible}>
+ * toggle
+ *
+ *
+ * {#if visible}
+ *
+ * flies in, unless the user prefers reduced motion
+ *
+ * {/if}
+ * ```
+ * @type {MediaQuery}
+ * @since 5.7.0
+ */
+export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
+ '(prefers-reduced-motion: reduce)'
+);
diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js
index 2afe64e71f61..270fabd4c774 100644
--- a/packages/svelte/src/motion/spring.js
+++ b/packages/svelte/src/motion/spring.js
@@ -160,6 +160,7 @@ export function spring(value, opts = {}) {
*
* ```
* @template T
+ * @since 5.8.0
*/
export class Spring {
#stiffness = source(0.15);
diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js
index bd43964062c8..d732dbc2831a 100644
--- a/packages/svelte/src/motion/tweened.js
+++ b/packages/svelte/src/motion/tweened.js
@@ -171,6 +171,7 @@ export function tweened(value, defaults = {}) {
*
* ```
* @template T
+ * @since 5.8.0
*/
export class Tween {
#current = source(/** @type {T} */ (undefined));
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
new file mode 100644
index 000000000000..63deca62ea8b
--- /dev/null
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -0,0 +1,81 @@
+import { get, tick, untrack } from '../internal/client/runtime.js';
+import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
+import { source } from '../internal/client/reactivity/sources.js';
+import { increment } from './utils.js';
+
+/**
+ * Returns a `subscribe` function that, if called in an effect (including expressions in the template),
+ * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs.
+ *
+ * If `start` returns a function, it will be called when the effect is destroyed.
+ *
+ * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
+ * are active, and the returned teardown function will only be called when all effects are destroyed.
+ *
+ * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery):
+ *
+ * ```js
+ * import { createSubscriber } from 'svelte/reactivity';
+ * import { on } from 'svelte/events';
+ *
+ * export class MediaQuery {
+ * #query;
+ * #subscribe;
+ *
+ * constructor(query) {
+ * this.#query = window.matchMedia(`(${query})`);
+ *
+ * this.#subscribe = createSubscriber((update) => {
+ * // when the `change` event occurs, re-run any effects that read `this.current`
+ * const off = on(this.#query, 'change', update);
+ *
+ * // stop listening when all the effects are destroyed
+ * return () => off();
+ * });
+ * }
+ *
+ * get current() {
+ * this.#subscribe();
+ *
+ * // Return the current state of the query, whether or not we're in an effect
+ * return this.#query.matches;
+ * }
+ * }
+ * ```
+ * @param {(update: () => void) => (() => void) | void} start
+ * @since 5.7.0
+ */
+export function createSubscriber(start) {
+ let subscribers = 0;
+ let version = source(0);
+ /** @type {(() => void) | void} */
+ let stop;
+
+ return () => {
+ if (effect_tracking()) {
+ get(version);
+
+ render_effect(() => {
+ if (subscribers === 0) {
+ stop = untrack(() => start(() => increment(version)));
+ }
+
+ subscribers += 1;
+
+ return () => {
+ tick().then(() => {
+ // Only count down after timeout, else we would reach 0 before our own render effect reruns,
+ // but reach 1 again when the tick callback of the prior teardown runs. That would mean we
+ // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
+ subscribers -= 1;
+
+ if (subscribers === 0) {
+ stop?.();
+ stop = undefined;
+ }
+ });
+ };
+ });
+ }
+ };
+}
diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js
index 2d8de624bcc7..33da2e176159 100644
--- a/packages/svelte/src/reactivity/date.js
+++ b/packages/svelte/src/reactivity/date.js
@@ -1,8 +1,7 @@
/** @import { Source } from '#client' */
-import { DESTROYED } from '../internal/client/constants.js';
import { derived } from '../internal/client/index.js';
import { source, set } from '../internal/client/reactivity/sources.js';
-import { get } from '../internal/client/runtime.js';
+import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js';
var inited = false;
@@ -12,6 +11,8 @@ export class SvelteDate extends Date {
/** @type {Map>} */
#deriveds = new Map();
+ #reaction = active_reaction;
+
/** @param {any[]} params */
constructor(...params) {
// @ts-ignore
@@ -43,7 +44,12 @@ export class SvelteDate extends Date {
var d = this.#deriveds.get(method);
- if (d === undefined || (d.f & DESTROYED) !== 0) {
+ if (d === undefined) {
+ // lazily create the derived, but as though it were being
+ // created at the same time as the class instance
+ const reaction = active_reaction;
+ set_active_reaction(this.#reaction);
+
d = derived(() => {
get(this.#time);
// @ts-ignore
@@ -51,6 +57,8 @@ export class SvelteDate extends Date {
});
this.#deriveds.set(method, d);
+
+ set_active_reaction(reaction);
}
return get(d);
diff --git a/packages/svelte/src/reactivity/date.test.ts b/packages/svelte/src/reactivity/date.test.ts
index 87bfde41c89c..f90c5a102c52 100644
--- a/packages/svelte/src/reactivity/date.test.ts
+++ b/packages/svelte/src/reactivity/date.test.ts
@@ -642,3 +642,33 @@ test('Date methods invoked for the first time in a derived', () => {
cleanup();
});
+
+test('Date methods shared between deriveds', () => {
+ const date = new SvelteDate(initial_date);
+ const log: any = [];
+
+ const cleanup = effect_root(() => {
+ const year = derived(() => {
+ return date.getFullYear();
+ });
+ const year2 = derived(() => {
+ return date.getTime(), date.getFullYear();
+ });
+
+ render_effect(() => {
+ log.push(get(year) + '/' + get(year2).toString());
+ });
+
+ flushSync(() => {
+ date.setFullYear(date.getFullYear() + 1);
+ });
+
+ flushSync(() => {
+ date.setFullYear(date.getFullYear() + 1);
+ });
+ });
+
+ assert.deepEqual(log, ['2023/2023', '2024/2024', '2025/2025']);
+
+ cleanup();
+});
diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js
index 2757688a5958..3eb9b95333ab 100644
--- a/packages/svelte/src/reactivity/index-client.js
+++ b/packages/svelte/src/reactivity/index-client.js
@@ -3,3 +3,5 @@ export { SvelteSet } from './set.js';
export { SvelteMap } from './map.js';
export { SvelteURL } from './url.js';
export { SvelteURLSearchParams } from './url-search-params.js';
+export { MediaQuery } from './media-query.js';
+export { createSubscriber } from './create-subscriber.js';
diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js
index 6240469ec36f..6a6c9dcf1360 100644
--- a/packages/svelte/src/reactivity/index-server.js
+++ b/packages/svelte/src/reactivity/index-server.js
@@ -3,3 +3,21 @@ export const SvelteSet = globalThis.Set;
export const SvelteMap = globalThis.Map;
export const SvelteURL = globalThis.URL;
export const SvelteURLSearchParams = globalThis.URLSearchParams;
+
+export class MediaQuery {
+ current;
+ /**
+ * @param {string} query
+ * @param {boolean} [matches]
+ */
+ constructor(query, matches = false) {
+ this.current = matches;
+ }
+}
+
+/**
+ * @param {any} _
+ */
+export function createSubscriber(_) {
+ return () => {};
+}
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
new file mode 100644
index 000000000000..a2be0adc91e2
--- /dev/null
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -0,0 +1,41 @@
+import { createSubscriber } from './create-subscriber.js';
+import { on } from '../events/index.js';
+
+/**
+ * Creates a media query and provides a `current` property that reflects whether or not it matches.
+ *
+ * Use it carefully — during server-side rendering, there is no way to know what the correct value should be, potentially causing content to change upon hydration.
+ * If you can use the media query in CSS to achieve the same effect, do that.
+ *
+ * ```svelte
+ *
+ *
+ *
{large.current ? 'large screen' : 'small screen'}
+ * ```
+ * @since 5.7.0
+ */
+export class MediaQuery {
+ #query;
+ #subscribe = createSubscriber((update) => {
+ return on(this.#query, 'change', update);
+ });
+
+ get current() {
+ this.#subscribe();
+
+ return this.#query.matches;
+ }
+
+ /**
+ * @param {string} query A media query string
+ * @param {boolean} [matches] Fallback value for the server
+ */
+ constructor(query, matches) {
+ // For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem
+ this.#query = window.matchMedia(`(${query})`);
+ }
+}
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index f2f1dfc4eba1..ae6806ec763f 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -1,14 +1,11 @@
/** @import { Readable, Writable } from './public.js' */
-import { noop } from '../internal/shared/utils.js';
import {
effect_root,
effect_tracking,
render_effect
} from '../internal/client/reactivity/effects.js';
-import { source } from '../internal/client/reactivity/sources.js';
-import { get as get_source, tick } from '../internal/client/runtime.js';
-import { increment } from '../reactivity/utils.js';
import { get, writable } from './shared/index.js';
+import { createSubscriber } from '../reactivity/create-subscriber.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -109,43 +106,23 @@ export function toStore(get, set) {
*/
export function fromStore(store) {
let value = /** @type {V} */ (undefined);
- let version = source(0);
- let subscribers = 0;
- let unsubscribe = noop;
+ const subscribe = createSubscriber((update) => {
+ let ran = false;
- function current() {
- if (effect_tracking()) {
- get_source(version);
+ const unsubscribe = store.subscribe((v) => {
+ value = v;
+ if (ran) update();
+ });
- render_effect(() => {
- if (subscribers === 0) {
- let ran = false;
-
- unsubscribe = store.subscribe((v) => {
- value = v;
- if (ran) increment(version);
- });
-
- ran = true;
- }
-
- subscribers += 1;
-
- return () => {
- tick().then(() => {
- // Only count down after timeout, else we would reach 0 before our own render effect reruns,
- // but reach 1 again when the tick callback of the prior teardown runs. That would mean we
- // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
- subscribers -= 1;
-
- if (subscribers === 0) {
- unsubscribe();
- }
- });
- };
- });
+ ran = true;
+
+ return unsubscribe;
+ });
+ function current() {
+ if (effect_tracking()) {
+ subscribe();
return value;
}
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 919660fd6a0a..75171c17865a 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -193,6 +193,8 @@ const ATTRIBUTE_ALIASES = {
nomodule: 'noModule',
playsinline: 'playsInline',
readonly: 'readOnly',
+ defaultvalue: 'defaultValue',
+ defaultchecked: 'defaultChecked',
srcobject: 'srcObject'
};
@@ -214,6 +216,8 @@ const DOM_PROPERTIES = [
'value',
'inert',
'volume',
+ 'defaultValue',
+ 'defaultChecked',
'srcObject'
];
@@ -224,7 +228,7 @@ export function is_dom_property(name) {
return DOM_PROPERTIES.includes(name);
}
-const NON_STATIC_PROPERTIES = ['autofocus', 'muted'];
+const NON_STATIC_PROPERTIES = ['autofocus', 'muted', 'defaultValue', 'defaultChecked'];
/**
* Returns `true` if the given attribute cannot be set through the template
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 1271d6cb90b7..331506a55a2f 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
-export const VERSION = '5.5.0';
+export const VERSION = '5.7.1';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 002ebf2e38ff..45b90240c99a 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -172,3 +172,16 @@ export function write(file, contents) {
fs.writeFileSync(file, contents);
}
+
+// Guard because not all test contexts load this with JSDOM
+if (typeof window !== 'undefined') {
+ // @ts-expect-error JS DOM doesn't support it
+ Window.prototype.matchMedia = (media) => {
+ return {
+ matches: false,
+ media,
+ addEventListener: () => {},
+ removeEventListener: () => {}
+ };
+ };
+}
diff --git a/packages/svelte/tests/motion/test.ts b/packages/svelte/tests/motion/test.ts
index 05971b5cab65..b6554e5e56ed 100644
--- a/packages/svelte/tests/motion/test.ts
+++ b/packages/svelte/tests/motion/test.ts
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+import '../helpers.js'; // for the matchMedia polyfill
import { describe, it, assert } from 'vitest';
import { get } from 'svelte/store';
import { spring, tweened } from 'svelte/motion';
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/_config.js b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/_config.js
new file mode 100644
index 000000000000..7a77424bb271
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/_config.js
@@ -0,0 +1,26 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ html: '
updatereset'
+ );
+
+ flushSync(() => reset.click());
+ },
+
+ warnings: [
+ 'A `$:` statement (main.svelte:4:1) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode'
+ ]
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/data.svelte.js b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/data.svelte.js
new file mode 100644
index 000000000000..70fd0a6abcd8
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/data.svelte.js
@@ -0,0 +1,11 @@
+export const obj = $state({
+ prop: 42
+});
+
+export function update() {
+ obj.prop += 1;
+}
+
+export function reset() {
+ obj.prop = 42;
+}
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/main.svelte b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/main.svelte
new file mode 100644
index 000000000000..2a9b1e1f2311
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-statement-non-reactive-import-statement/main.svelte
@@ -0,0 +1,13 @@
+
+
+
{a}
+
{b}
+update
+reset
diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-increment-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/bigint-increment-mutation/_config.js
new file mode 100644
index 000000000000..fe1e962de23b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bigint-increment-mutation/_config.js
@@ -0,0 +1,15 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: 'mutatereassign
` (h1.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.',
- 'node_invalid_placement_ssr: `
` (main.svelte:9:0) cannot contain `
` (form.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.'
+ 'node_invalid_placement_ssr: `
` (h1.svelte:1:0) cannot be a child of `
` (main.svelte:6:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.',
+ 'node_invalid_placement_ssr: `
` (form.svelte:1:0) cannot be a child of `
` (main.svelte:9:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
],
warnings: [
diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-actions/_config.js b/packages/svelte/tests/runtime-runes/samples/nullish-actions/_config.js
new file mode 100644
index 000000000000..5d96dc96a481
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/nullish-actions/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: ' '
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-actions/main.svelte b/packages/svelte/tests/runtime-runes/samples/nullish-actions/main.svelte
new file mode 100644
index 000000000000..2cbeb6722d1a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/nullish-actions/main.svelte
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/_config.js
new file mode 100644
index 000000000000..a6d79c05ed31
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/_config.js
@@ -0,0 +1,24 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ html: `items: null`,
+
+ test({ assert, target, warnings }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, `items: []`);
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, `items: [0]`);
+
+ assert.deepEqual(warnings, [
+ 'Assignment to `items` property (main.svelte:5:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/main.svelte
new file mode 100644
index 000000000000..f151336046f1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment-warning/main.svelte
@@ -0,0 +1,7 @@
+
+
+ (object.items ??= []).push(object.items.length)}>
+ items: {JSON.stringify(object.items)}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/_config.js
new file mode 100644
index 000000000000..99d957e980fe
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `items: null`,
+
+ test({ assert, target }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ flushSync(() => btn1.click());
+ assert.htmlEqual(target.innerHTML, `items: [0]`);
+
+ flushSync(() => btn1.click());
+ assert.htmlEqual(target.innerHTML, `items: [0,1]`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/main.svelte
new file mode 100644
index 000000000000..84c1c32c5cc8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-nullish-coalescing-assignment/main.svelte
@@ -0,0 +1,7 @@
+
+
+ (items ??= []).push(items.length)}>
+ items: {JSON.stringify(items)}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/_config.js b/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/_config.js
new file mode 100644
index 000000000000..9cff16d9f54b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/_config.js
@@ -0,0 +1,32 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, instance }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ const input = target.querySelector('input');
+ ok(p);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p.innerHTML, '1');
+
+ flushSync(() => {
+ input?.click();
+ });
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p.innerHTML, '1');
+
+ flushSync(() => {
+ input?.click();
+ });
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p.innerHTML, '2');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/main.svelte b/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/main.svelte
new file mode 100644
index 000000000000..4ca6e613392b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/remove-spreaded-handlers/main.svelte
@@ -0,0 +1,12 @@
+
+
+
` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:2:1)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
+ '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/samples/invalid-nested-svelte-element/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html
index 27c37f693baa..6d9ea9de5f77 100644
--- a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html
+++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json
index 3d9786582343..abbded296af1 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement",
- "message": "`
` is invalid inside `
`",
+ "message": "`
` cannot be a descendant of `
`. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.",
"start": {
"line": 4,
"column": 3
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json
index 4d637aed805a..727bf6c258ca 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement",
- "message": "`
` is invalid inside `
`",
+ "message": "`
` cannot be a descendant of `
`. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.",
"start": {
"line": 4,
"column": 3
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json b/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json
index e85050beb721..59c73c4e736d 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement_ssr",
- "message": "`
` is invalid inside `
`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning",
+ "message": "`
` cannot be a child of `
`. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning",
"start": {
"line": 4,
"column": 3
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json
index 63fc9c517e47..4849717e146d 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement",
- "message": "`
` is invalid inside `
`",
+ "message": "`
` cannot be a descendant of `
`. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.",
"start": {
"line": 16,
"column": 3
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-7/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-7/errors.json
index edfc158c9d56..a1422c001a0c 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-7/errors.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-7/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement",
- "message": "`` is invalid inside `
`",
+ "message": "`` must be the child of a `
`, not a `
`. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.",
"start": {
"line": 8,
"column": 1
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-8/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-8/errors.json
index 1127a7633624..d75d5d410521 100644
--- a/packages/svelte/tests/validator/samples/invalid-node-placement-8/errors.json
+++ b/packages/svelte/tests/validator/samples/invalid-node-placement-8/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "node_invalid_placement",
- "message": "Text node is invalid inside ``",
+ "message": "`<#text>` cannot be a child of ``. `` only allows these children: `