diff --git a/.changeset/silly-apples-remain.md b/.changeset/silly-apples-remain.md
deleted file mode 100644
index 10d43db550..0000000000
--- a/.changeset/silly-apples-remain.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: handle more hydration mismatches
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index aac006d2f5..f7314e2950 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -67,16 +67,15 @@ todos[0].done = !todos[0].done;
### Classes
-You can also use `$state` in class fields (whether public or private):
+You can also use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
```js
// @errors: 7006 2554
class Todo {
done = $state(false);
- text = $state();
constructor(text) {
- this.text = text;
+ this.text = $state(text);
}
reset() {
@@ -110,10 +109,9 @@ You can either use an inline function...
// @errors: 7006 2554
class Todo {
done = $state(false);
- text = $state();
constructor(text) {
- this.text = text;
+ this.text = $state(text);
}
+++reset = () => {+++
diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md
index 1736a82e77..0d43ab6cfa 100644
--- a/documentation/docs/03-template-syntax/09-@attach.md
+++ b/documentation/docs/03-template-syntax/09-@attach.md
@@ -2,7 +2,9 @@
title: {@attach ...}
---
-Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when the element is later removed from the DOM.
+Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
+
+Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
> [!NOTE]
> Attachments are available in Svelte 5.29 and newer.
@@ -55,7 +57,7 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
```
-Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes.
+Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
@@ -126,9 +128,42 @@ This allows you to create _wrapper components_ that augment elements ([demo](/pl
```
-### Converting actions to attachments
+## Controlling when attachments re-run
+
+Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
+
+```js
+// @errors: 7006 2304 2552
+function foo(bar) {
+ return (node) => {
+ veryExpensiveSetupWork(node);
+ update(node, bar);
+ };
+}
+```
+
+In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
+
+```js
+// @errors: 7006 2304 2552
+function foo(+++getBar+++) {
+ return (node) => {
+ veryExpensiveSetupWork(node);
+
++++ $effect(() => {
+ update(node, getBar());
+ });+++
+ }
+}
+```
+
+## Creating attachments programmatically
+
+To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
-If you want to use this functionality on Components but you are using a library that only provides actions you can use the `fromAction` utility exported from `svelte/attachments` to convert between the two.
+## Converting actions to attachments
+
+If you want to use this functionality on components but you are using a library that only provides actions you can use the [`fromAction`](svelte/attachments#fromAction) utility.
This function accept an action as the first argument and a function returning the arguments of the action as the second argument and returns an attachment.
@@ -146,9 +181,4 @@ This function accept an action as the first argument and a function returning th
{@attach fromAction(log, () => count)}
>
{count}
-
-```
-
-## Creating attachments programmatically
-
-To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
+
\ No newline at end of file
diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md
index 4204bcfe6d..f395de421c 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -125,7 +125,7 @@ In many cases this is perfectly fine, but there is a risk: if you mutate the sta
```svelte
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js
new file mode 100644
index 0000000000..dd847ce2f2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+ ssrHtml: ``,
+
+ async test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte
new file mode 100644
index 0000000000..47b8c901eb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js
new file mode 100644
index 0000000000..f47bee71df
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte
new file mode 100644
index 0000000000..e2c4f302b3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js
new file mode 100644
index 0000000000..4cf1aea213
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js
@@ -0,0 +1,45 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ // The component context class instance gets shared between tests, strangely, causing hydration to fail?
+ mode: ['client', 'server'],
+
+ async test({ assert, target, logs }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]);
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]);
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]);
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.deepEqual(logs, [
+ 0,
+ 'class trigger false',
+ 'local trigger false',
+ 1,
+ 2,
+ 3,
+ 4,
+ 'class trigger true',
+ 'local trigger true'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
new file mode 100644
index 0000000000..03687d01bb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js
new file mode 100644
index 0000000000..02cf36d900
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte
new file mode 100644
index 0000000000..5dbbb10afd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js
new file mode 100644
index 0000000000..32cca6c693
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
new file mode 100644
index 0000000000..d8feb554cd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js
new file mode 100644
index 0000000000..f35dc57228
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
new file mode 100644
index 0000000000..aa8ba1658b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js
index f7a4ca05f5..d8b202955a 100644
--- a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js
@@ -5,5 +5,10 @@ export default test({
async test({ window }) {
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)');
expect(window.matchMedia).toHaveBeenCalledWith('(min-width: 900px)');
+ expect(window.matchMedia).toHaveBeenCalledWith('screen');
+ expect(window.matchMedia).toHaveBeenCalledWith('not print');
+ expect(window.matchMedia).toHaveBeenCalledWith('screen,print');
+ expect(window.matchMedia).toHaveBeenCalledWith('screen, print');
+ expect(window.matchMedia).toHaveBeenCalledWith('screen, random');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte
index 446a9213dd..fe07ed8ab0 100644
--- a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte
@@ -3,4 +3,9 @@
const mq = new MediaQuery("(max-width: 599px), (min-width: 900px)");
const mq2 = new MediaQuery("min-width: 900px");
+ const mq3 = new MediaQuery("screen");
+ const mq4 = new MediaQuery("not print");
+ const mq5 = new MediaQuery("screen,print");
+ const mq6 = new MediaQuery("screen, print");
+ const mq7 = new MediaQuery("screen, random");
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json
new file mode 100644
index 0000000000..82765c51c1
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_duplicate",
+ "message": "`count` has already been declared on this class",
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 24
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js
new file mode 100644
index 0000000000..05cd4d9d9d
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ count = $state(0);
+
+ constructor() {
+ this.count = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json
new file mode 100644
index 0000000000..c4cb0991d0
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_invalid_assignment",
+ "message": "Cannot assign to a state field before its declaration",
+ "start": {
+ "line": 4,
+ "column": 3
+ },
+ "end": {
+ "line": 4,
+ "column": 18
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js
new file mode 100644
index 0000000000..e5ad562727
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js
@@ -0,0 +1,9 @@
+export class Counter {
+ constructor() {
+ if (true) {
+ this.count = -1;
+ }
+
+ this.count = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json
new file mode 100644
index 0000000000..82765c51c1
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_duplicate",
+ "message": "`count` has already been declared on this class",
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 24
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js
new file mode 100644
index 0000000000..e37be4b3e6
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ constructor() {
+ this.count = $state(0);
+ this.count = 1;
+ this.count = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json
new file mode 100644
index 0000000000..175c41f98c
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_duplicate",
+ "message": "`count` has already been declared on this class",
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 28
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js
new file mode 100644
index 0000000000..f9196ff3cd
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ constructor() {
+ this.count = $state(0);
+ this.count = 1;
+ this.count = $state.raw(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json
new file mode 100644
index 0000000000..9f959874c8
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_invalid_placement",
+ "message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
+ "start": {
+ "line": 4,
+ "column": 16
+ },
+ "end": {
+ "line": 4,
+ "column": 25
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js
new file mode 100644
index 0000000000..bf1aada1b5
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ constructor() {
+ if (true) {
+ this.count = $state(0);
+ }
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
new file mode 100644
index 0000000000..af2f30dade
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_duplicate",
+ "message": "`count` has already been declared on this class",
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 27
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js
new file mode 100644
index 0000000000..bc3d19a14f
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ // prettier-ignore
+ 'count' = $state(0);
+ constructor() {
+ this['count'] = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json
new file mode 100644
index 0000000000..ae7a47f31b
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_duplicate",
+ "message": "`count` has already been declared on this class",
+ "start": {
+ "line": 4,
+ "column": 2
+ },
+ "end": {
+ "line": 4,
+ "column": 27
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js
new file mode 100644
index 0000000000..2ebe52e685
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js
@@ -0,0 +1,6 @@
+export class Counter {
+ count = $state(0);
+ constructor() {
+ this['count'] = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json
new file mode 100644
index 0000000000..64e56f8d5c
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_invalid_placement",
+ "message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
+ "start": {
+ "line": 5,
+ "column": 16
+ },
+ "end": {
+ "line": 5,
+ "column": 25
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js
new file mode 100644
index 0000000000..50c8559837
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js
@@ -0,0 +1,7 @@
+const count = 'count';
+
+export class Counter {
+ constructor() {
+ this[count] = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json
new file mode 100644
index 0000000000..2e0bd10ff8
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_invalid_assignment",
+ "message": "Cannot assign to a state field before its declaration",
+ "start": {
+ "line": 3,
+ "column": 2
+ },
+ "end": {
+ "line": 3,
+ "column": 17
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js
new file mode 100644
index 0000000000..0a76c6fec9
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js
@@ -0,0 +1,6 @@
+export class Counter {
+ constructor() {
+ this.count = -1;
+ this.count = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json
new file mode 100644
index 0000000000..b7dd4c8ed4
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_field_invalid_assignment",
+ "message": "Cannot assign to a state field before its declaration",
+ "start": {
+ "line": 2,
+ "column": 1
+ },
+ "end": {
+ "line": 2,
+ "column": 12
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js
new file mode 100644
index 0000000000..a8469e13af
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+ count = -1;
+
+ constructor() {
+ this.count = $state(0);
+ }
+}
diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
index 32594e4268..e1906b181a 100644
--- a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
+++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "state_invalid_placement",
- "message": "`$derived(...)` can only be used as a variable declaration initializer or a class field",
+ "message": "`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
"start": {
"line": 2,
"column": 15
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 5520ccabe9..fa0d701422 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1420,7 +1420,13 @@ declare module 'svelte/compiler' {
| AST.SvelteWindow
| AST.SvelteBoundary;
- export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
+ export type Tag =
+ | AST.AttachTag
+ | AST.ConstTag
+ | AST.DebugTag
+ | AST.ExpressionTag
+ | AST.HtmlTag
+ | AST.RenderTag;
export type TemplateNode =
| AST.Root