diff --git a/.changeset/wild-bulldogs-move.md b/.changeset/wild-bulldogs-move.md
deleted file mode 100644
index c3c5580f77..0000000000
--- a/.changeset/wild-bulldogs-move.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: allow characters in the supplementary special-purpose plane
diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js
index 9d8d279c35..8f38686a29 100644
--- a/benchmarking/compare/index.js
+++ b/benchmarking/compare/index.js
@@ -67,19 +67,29 @@ for (let i = 0; i < results[0].length; i += 1) {
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
+ let max = -Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
- if (times[b] < min) {
- min = times[b];
+ const time = times[b];
+
+ if (time < min) {
+ min = time;
min_index = b;
}
+
+ if (time > max) {
+ max = time;
+ }
}
if (min !== 0) {
- console.group(`${metric}: fastest is ${branches[min_index]}`);
+ console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
- console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
+ const SIZE = 20;
+ const n = Math.round(SIZE * (time / max));
+
+ console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
});
console.groupEnd();
}
@@ -87,3 +97,7 @@ for (let i = 0; i < results[0].length; i += 1) {
console.groupEnd();
}
+
+function char(i) {
+ return String.fromCharCode(97 + i);
+}
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index 16630a977b..f7314e2950 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -20,7 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
-> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity).
+> [!NOTE] Class instances are not proxied. You can create [reactive state fields](#Classes) on classes that you define. Svelte provides reactive implementations of built-ins like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
@@ -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/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md
index ff3d64757b..13ac8b79a3 100644
--- a/documentation/docs/02-runes/07-$inspect.md
+++ b/documentation/docs/02-runes/07-$inspect.md
@@ -52,6 +52,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve
import { doSomeWork } from './elsewhere';
$effect(() => {
+ +++// $inspect.trace must be the first statement of a function body+++
+++$inspect.trace();+++
doSomeWork();
});
diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md
index fe5f8b02aa..feecfe033e 100644
--- a/documentation/docs/03-template-syntax/01-basic-markup.md
+++ b/documentation/docs/03-template-syntax/01-basic-markup.md
@@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
```
+## Spread attributes
+
_Spread attributes_ allow many attributes or properties to be passed to an element or component at once.
-An element or component can have multiple spread attributes, interspersed with regular ones.
+An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`:
```svelte
-
+
```
## Events
diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md
index 70666f6a57..006cadd152 100644
--- a/documentation/docs/03-template-syntax/03-each.md
+++ b/documentation/docs/03-template-syntax/03-each.md
@@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in
{#each expression as name, index (key)}...{/each}
```
-If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
+If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
+
+The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```svelte
{#each items as item (item.id)}
diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md
new file mode 100644
index 0000000000..b25fbb32a6
--- /dev/null
+++ b/documentation/docs/03-template-syntax/09-@attach.md
@@ -0,0 +1,166 @@
+---
+title: {@attach ...}
+---
+
+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.
+
+```svelte
+
+
+
+
...
+```
+
+An element can have any number of attachments.
+
+## Attachment factories
+
+A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
+
+```svelte
+
+
+
+
+
+
+ Hover me
+
+```
+
+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
+
+Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
+
+```svelte
+
+ {
+ const context = canvas.getContext('2d');
+
+ $effect(() => {
+ context.fillStyle = color;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ });
+ }}
+>
+```
+
+> [!NOTE]
+> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
+
+## Passing attachments to components
+
+When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
+
+This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
+
+```svelte
+
+
+
+
+
+ {@render children?.()}
+
+```
+
+```svelte
+
+
+
+
+
+
+ Hover me
+
+```
+
+## 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).
+
+## Converting actions to attachments
+
+If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
diff --git a/documentation/docs/03-template-syntax/09-@const.md b/documentation/docs/03-template-syntax/10-@const.md
similarity index 100%
rename from documentation/docs/03-template-syntax/09-@const.md
rename to documentation/docs/03-template-syntax/10-@const.md
diff --git a/documentation/docs/03-template-syntax/10-@debug.md b/documentation/docs/03-template-syntax/11-@debug.md
similarity index 100%
rename from documentation/docs/03-template-syntax/10-@debug.md
rename to documentation/docs/03-template-syntax/11-@debug.md
diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/12-bind.md
similarity index 90%
rename from documentation/docs/03-template-syntax/11-bind.md
rename to documentation/docs/03-template-syntax/12-bind.md
index c23f3b5232..0970ee384e 100644
--- a/documentation/docs/03-template-syntax/11-bind.md
+++ b/documentation/docs/03-template-syntax/12-bind.md
@@ -117,6 +117,29 @@ Since 5.6.0, if an ` ` has a `defaultChecked` attribute and is part of a f
```
+## ` `
+
+Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:
+
+```svelte
+
+
+
+```
+
## ` `
Inputs that work together can use `bind:group`.
@@ -227,6 +250,7 @@ You can give the `` a default value by adding a `selected` attribute to
- [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event)
- [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended)
- [`readyState`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState)
+- [`played`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/played)
```svelte
@@ -254,6 +278,10 @@ You can give the `` a default value by adding a `selected` attribute to
```
+## `window` and `document`
+
+To bind to properties of `window` and `document`, see [``](svelte-window) and [``](svelte-document).
+
## Contenteditable bindings
Elements with the `contenteditable` attribute support the following bindings:
@@ -278,6 +306,10 @@ All visible elements have the following readonly bindings, measured with a `Resi
- [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight)
- [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth)
- [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight)
+- [`contentRect`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentRect)
+- [`contentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize)
+- [`borderBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize)
+- [`devicePixelContentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/devicePixelContentBoxSize)
```svelte
@@ -285,7 +317,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
```
-> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like ` ` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`.
+> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like ` ` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. Note that CSS transformations do not trigger `ResizeObserver` callbacks.
## bind:this
diff --git a/documentation/docs/03-template-syntax/12-use.md b/documentation/docs/03-template-syntax/13-use.md
similarity index 93%
rename from documentation/docs/03-template-syntax/12-use.md
rename to documentation/docs/03-template-syntax/13-use.md
index 45de023578..5f5321a1c0 100644
--- a/documentation/docs/03-template-syntax/12-use.md
+++ b/documentation/docs/03-template-syntax/13-use.md
@@ -2,6 +2,9 @@
title: use:
---
+> [!NOTE]
+> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable.
+
Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted:
```svelte
diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/14-transition.md
similarity index 100%
rename from documentation/docs/03-template-syntax/13-transition.md
rename to documentation/docs/03-template-syntax/14-transition.md
diff --git a/documentation/docs/03-template-syntax/14-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md
similarity index 100%
rename from documentation/docs/03-template-syntax/14-in-and-out.md
rename to documentation/docs/03-template-syntax/15-in-and-out.md
diff --git a/documentation/docs/03-template-syntax/15-animate.md b/documentation/docs/03-template-syntax/16-animate.md
similarity index 100%
rename from documentation/docs/03-template-syntax/15-animate.md
rename to documentation/docs/03-template-syntax/16-animate.md
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
+ *
+ * click me
+ * ```
+ * @since 5.29
+ */
+export function createAttachmentKey() {
+ return Symbol(ATTACHMENT_KEY);
+}
+
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ * ...
+ *
+ *
+ * bar)}>...
+ * ```
+ * @template {EventTarget} E
+ * @template {unknown} T
+ * @overload
+ * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function
+ * @param {() => T} fn A function that returns the argument for the action
+ * @returns {Attachment}
+ */
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ * ...
+ *
+ *
+ * bar)}>...
+ * ```
+ * @template {EventTarget} E
+ * @overload
+ * @param {Action | ((element: E) => void | ActionReturn)} action The action function
+ * @returns {Attachment}
+ */
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ * ...
+ *
+ *
+ * bar)}>...
+ * ```
+ *
+ * @template {EventTarget} E
+ * @template {unknown} T
+ * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function
+ * @param {() => T} fn A function that returns the argument for the action
+ * @returns {Attachment}
+ * @since 5.32
+ */
+export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
+ return (element) => {
+ const { update, destroy } = untrack(() => action(element, fn()) ?? {});
+
+ if (update) {
+ var ran = false;
+ render_effect(() => {
+ const arg = fn();
+ if (ran) update(arg);
+ });
+ ran = true;
+ }
+
+ if (destroy) {
+ teardown(destroy);
+ }
+ };
+}
diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts
new file mode 100644
index 0000000000..caf1342d0a
--- /dev/null
+++ b/packages/svelte/src/attachments/public.d.ts
@@ -0,0 +1,12 @@
+/**
+ * An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
+ * to the DOM, and optionally returns a function that is called when the element is later removed.
+ *
+ * It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
+ * a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
+ */
+export interface Attachment {
+ (element: T): void | (() => void);
+}
+
+export * from './index.js';
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 043b3cf587..9a22a89418 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -479,6 +479,25 @@ export function snippet_parameter_assignment(node) {
e(node, 'snippet_parameter_assignment', `Cannot reassign or bind to snippet parameter\nhttps://svelte.dev/e/snippet_parameter_assignment`);
}
+/**
+ * `%name%` has already been declared on this class
+ * @param {null | number | NodeLike} node
+ * @param {string} name
+ * @returns {never}
+ */
+export function state_field_duplicate(node, name) {
+ e(node, 'state_field_duplicate', `\`${name}\` has already been declared on this class\nhttps://svelte.dev/e/state_field_duplicate`);
+}
+
+/**
+ * Cannot assign to a state field before its declaration
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function state_field_invalid_assignment(node) {
+ e(node, 'state_field_invalid_assignment', `Cannot assign to a state field before its declaration\nhttps://svelte.dev/e/state_field_invalid_assignment`);
+}
+
/**
* Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
* @param {null | number | NodeLike} node
@@ -489,13 +508,13 @@ export function state_invalid_export(node) {
}
/**
- * `%rune%(...)` can only be used as a variable declaration initializer or a class field
+ * `%rune%(...)` 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.
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function state_invalid_placement(node, rune) {
- e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
+ e(node, 'state_invalid_placement', `\`${rune}(...)\` 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.\nhttps://svelte.dev/e/state_invalid_placement`);
}
/**
diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js
index e3f88c8f1d..f6b7e4b054 100644
--- a/packages/svelte/src/compiler/legacy.js
+++ b/packages/svelte/src/compiler/legacy.js
@@ -378,7 +378,8 @@ export function convert(source, ast) {
end: node.end,
expression: node.expression,
parameters: node.parameters,
- children: node.body.nodes.map((child) => visit(child))
+ children: node.body.nodes.map((child) => visit(child)),
+ typeParams: node.typeParams
};
},
// @ts-expect-error
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index c28602c4db..6775f3dc97 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -1310,7 +1310,7 @@ const template = {
name = state.scope.generate(slot_name);
if (name !== slot_name) {
throw new MigrationError(
- 'This migration would change the name of a slot making the component unusable'
+ `This migration would change the name of a slot (${slot_name} to ${name}) making the component unusable`
);
}
}
@@ -1883,7 +1883,7 @@ function handle_identifier(node, state, path) {
let new_name = state.scope.generate(name);
if (new_name !== name) {
throw new MigrationError(
- 'This migration would change the name of a slot making the component unusable'
+ `This migration would change the name of a slot (${name} to ${new_name}) making the component unusable`
);
}
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js
index f4c73dcf40..b118901830 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/context.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js
@@ -1,7 +1,7 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
-import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js';
+import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@@ -33,7 +33,9 @@ export default function read_pattern(parser) {
};
}
- if (!is_bracket_open(parser.template[i])) {
+ const char = parser.template[i];
+
+ if (char !== '{' && char !== '[') {
e.expected_pattern(i);
}
@@ -71,75 +73,6 @@ export default function read_pattern(parser) {
}
}
-/**
- * @param {Parser} parser
- * @param {number} start
- */
-function match_bracket(parser, start) {
- const bracket_stack = [];
-
- let i = start;
-
- while (i < parser.template.length) {
- let char = parser.template[i++];
-
- if (char === "'" || char === '"' || char === '`') {
- i = match_quote(parser, i, char);
- continue;
- }
-
- if (is_bracket_open(char)) {
- bracket_stack.push(char);
- } else if (is_bracket_close(char)) {
- const popped = /** @type {string} */ (bracket_stack.pop());
- const expected = /** @type {string} */ (get_bracket_close(popped));
-
- if (char !== expected) {
- e.expected_token(i - 1, expected);
- }
-
- if (bracket_stack.length === 0) {
- return i;
- }
- }
- }
-
- e.unexpected_eof(parser.template.length);
-}
-
-/**
- * @param {Parser} parser
- * @param {number} start
- * @param {string} quote
- */
-function match_quote(parser, start, quote) {
- let is_escaped = false;
- let i = start;
-
- while (i < parser.template.length) {
- const char = parser.template[i++];
-
- if (is_escaped) {
- is_escaped = false;
- continue;
- }
-
- if (char === quote) {
- return i;
- }
-
- if (char === '\\') {
- is_escaped = true;
- }
-
- if (quote === '`' && char === '$' && parser.template[i] === '{') {
- i = match_bracket(parser, i);
- }
- }
-
- e.unterminated_string_constant(start);
-}
-
/**
* @param {Parser} parser
* @returns {any}
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js
index 9d9ed3a1ef..6290127811 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/script.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js
@@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
- * @param {Array} attributes
+ * @param {Array} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js
index 56dbe124b7..e15a47e6d5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
- * @param {Array} attributes
+ * @param {Array} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js
index b18e1cb25b..87332f647d 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/element.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js
@@ -93,7 +93,16 @@ export default function element(parser) {
}
}
- if (parent.type !== 'RegularElement' && !parser.loose) {
+ if (parent.type === 'RegularElement') {
+ if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
+ const end = parent.fragment.nodes[0]?.start ?? start;
+ w.element_implicitly_closed(
+ { start: parent.start, end },
+ `${name}>`,
+ `${parent.name}>`
+ );
+ }
+ } else if (!parser.loose) {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
@@ -186,6 +195,8 @@ export default function element(parser) {
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
+ const end = parent.fragment.nodes[0]?.start ?? start;
+ w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, `${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
@@ -482,7 +493,7 @@ function read_static_attribute(parser) {
/**
* @param {Parser} parser
- * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
+ * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
const start = parser.index;
@@ -490,6 +501,27 @@ function read_attribute(parser) {
if (parser.eat('{')) {
parser.allow_whitespace();
+ if (parser.eat('@attach')) {
+ parser.require_whitespace();
+
+ const expression = read_expression(parser);
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ /** @type {AST.AttachTag} */
+ const attachment = {
+ type: 'AttachTag',
+ start,
+ end: parser.index,
+ expression,
+ metadata: {
+ expression: create_expression_metadata()
+ }
+ };
+
+ return attachment;
+ }
+
if (parser.eat('...')) {
const expression = read_expression(parser);
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
index 90440e0980..fa6e666343 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
@@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
+import { match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
+const pointy_bois = { '<': '>' };
+
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
@@ -357,6 +360,22 @@ function open(parser) {
const params_start = parser.index;
+ // snippets could have a generic signature, e.g. `#snippet foo(...)`
+ /** @type {string | undefined} */
+ let type_params;
+
+ // if we match a generic opening
+ if (parser.ts && parser.match('<')) {
+ const start = parser.index;
+ const end = match_bracket(parser, start, pointy_bois);
+
+ type_params = parser.template.slice(start + 1, end - 1);
+
+ parser.index = end;
+ }
+
+ parser.allow_whitespace();
+
const matched = parser.eat('(', true, false);
if (matched) {
@@ -394,6 +413,7 @@ function open(parser) {
end: name_end,
name
},
+ typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
metadata: {
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js
index b7c8cb43cd..8c69a58c99 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js
@@ -1,34 +1,5 @@
-const SQUARE_BRACKET_OPEN = '[';
-const SQUARE_BRACKET_CLOSE = ']';
-const CURLY_BRACKET_OPEN = '{';
-const CURLY_BRACKET_CLOSE = '}';
-const PARENTHESES_OPEN = '(';
-const PARENTHESES_CLOSE = ')';
-
-/** @param {string} char */
-export function is_bracket_open(char) {
- return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
-}
-
-/** @param {string} char */
-export function is_bracket_close(char) {
- return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
-}
-
-/** @param {string} open */
-export function get_bracket_close(open) {
- if (open === SQUARE_BRACKET_OPEN) {
- return SQUARE_BRACKET_CLOSE;
- }
-
- if (open === CURLY_BRACKET_OPEN) {
- return CURLY_BRACKET_CLOSE;
- }
-
- if (open === PARENTHESES_OPEN) {
- return PARENTHESES_CLOSE;
- }
-}
+/** @import { Parser } from '../index.js' */
+import * as e from '../../../errors.js';
/**
* @param {number} num
@@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) {
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
- const close = get_bracket_close(open);
+ const close = default_brackets[open];
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
@@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) {
}
return undefined;
}
+
+/** @type {Record} */
+const default_brackets = {
+ '{': '}',
+ '(': ')',
+ '[': ']'
+};
+
+/**
+ * @param {Parser} parser
+ * @param {number} start
+ * @param {Record} brackets
+ */
+export function match_bracket(parser, start, brackets = default_brackets) {
+ const close = Object.values(brackets);
+ const bracket_stack = [];
+
+ let i = start;
+
+ while (i < parser.template.length) {
+ let char = parser.template[i++];
+
+ if (char === "'" || char === '"' || char === '`') {
+ i = match_quote(parser, i, char);
+ continue;
+ }
+
+ if (char in brackets) {
+ bracket_stack.push(char);
+ } else if (close.includes(char)) {
+ const popped = /** @type {string} */ (bracket_stack.pop());
+ const expected = /** @type {string} */ (brackets[popped]);
+
+ if (char !== expected) {
+ e.expected_token(i - 1, expected);
+ }
+
+ if (bracket_stack.length === 0) {
+ return i;
+ }
+ }
+ }
+
+ e.unexpected_eof(parser.template.length);
+}
+
+/**
+ * @param {Parser} parser
+ * @param {number} start
+ * @param {string} quote
+ */
+function match_quote(parser, start, quote) {
+ let is_escaped = false;
+ let i = start;
+
+ while (i < parser.template.length) {
+ const char = parser.template[i++];
+
+ if (is_escaped) {
+ is_escaped = false;
+ continue;
+ }
+
+ if (char === quote) {
+ return i;
+ }
+
+ if (char === '\\') {
+ is_escaped = true;
+ }
+
+ if (quote === '`' && char === '$' && parser.template[i] === '{') {
+ i = match_bracket(parser, i);
+ }
+ }
+
+ e.unterminated_string_constant(start);
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 20b338594a..d99f1a84d6 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -18,6 +18,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
+import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
@@ -44,9 +45,11 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
+import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
+import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@@ -64,6 +67,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
+import { TemplateElement } from './visitors/TemplateElement.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
@@ -132,6 +136,7 @@ const visitors = {
},
ArrowFunctionExpression,
AssignmentExpression,
+ AttachTag,
Attribute,
AwaitBlock,
AwaitExpression,
@@ -158,9 +163,11 @@ const visitors = {
KeyBlock,
LabeledStatement,
LetDirective,
+ Literal,
MemberExpression,
NewExpression,
OnDirective,
+ PropertyDefinition,
RegularElement,
RenderTag,
SlotElement,
@@ -178,6 +185,7 @@ const visitors = {
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
+ TemplateElement,
Text,
TransitionDirective,
TitleElement,
@@ -259,7 +267,8 @@ export function analyze_module(ast, options) {
immutable: true,
tracing: false,
async_deriveds: new Set(),
- context_preserving_awaits: new Set()
+ context_preserving_awaits: new Set(),
+ classes: new Map()
};
walk(
@@ -268,7 +277,7 @@ export function analyze_module(ast, options) {
scope,
scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis),
- derived_state: [],
+ state_fields: new Map(),
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
@@ -439,6 +448,7 @@ export function analyze_component(root, source, options) {
elements: [],
runes,
tracing: false,
+ classes: new Map(),
immutable: runes || options.immutable,
exports: [],
uses_props: false,
@@ -636,7 +646,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
- derived_state: [],
+ state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
};
@@ -703,7 +713,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null,
component_slots: new Set(),
expression: null,
- derived_state: [],
+ state_fields: new Map(),
function_depth: scope.function_depth
};
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index 17c8123de1..080239bac0 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -1,6 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
-import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
+import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
export interface AnalysisState {
scope: Scope;
@@ -18,7 +18,10 @@ export interface AnalysisState {
component_slots: Set;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
- derived_state: { name: string; private: boolean }[];
+
+ /** Used to analyze class state */
+ state_fields: Map;
+
function_depth: number;
// legacy stuff
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
index a64c89cd88..673c79f2df 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
@@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
- validate_assignment(node, node.left, context.state);
+ validate_assignment(node, node.left, context);
if (context.state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js
new file mode 100644
index 0000000000..1e318f228d
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js
@@ -0,0 +1,13 @@
+/** @import { AST } from '#compiler' */
+/** @import { Context } from '../types' */
+
+import { mark_subtree_dynamic } from './shared/fragment.js';
+
+/**
+ * @param {AST.AttachTag} node
+ * @param {Context} context
+ */
+export function AttachTag(node, context) {
+ mark_subtree_dynamic(context.path);
+ context.next({ ...context.state, expression: node.metadata.expression });
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
index 3ba81767cc..773aa59744 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -211,7 +211,7 @@ function get_delegated_event(event_name, handler, context) {
if (
binding !== null &&
- // Bail out if the the binding is a rest param
+ // Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
index 18ea79262b..9f02e7fa5a 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
@@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
- validate_assignment(node, node.expression, context.state);
+ validate_assignment(node, node.expression, context);
const assignee = node.expression;
const left = object(assignee);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 4f78485fbd..9b6337b9ed 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -115,12 +115,13 @@ export function CallExpression(node, context) {
case '$state':
case '$state.raw':
case '$derived':
- case '$derived.by':
- if (
- (parent.type !== 'VariableDeclarator' ||
- get_parent(context.path, -3).type === 'ConstTag') &&
- !(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
- ) {
+ case '$derived.by': {
+ const valid =
+ is_variable_declaration(parent, context) ||
+ is_class_property_definition(parent) ||
+ is_class_property_assignment_at_constructor_root(parent, context);
+
+ if (!valid) {
e.state_invalid_placement(node, rune);
}
@@ -131,6 +132,7 @@ export function CallExpression(node, context) {
}
break;
+ }
case '$effect':
case '$effect.pre':
@@ -290,3 +292,40 @@ function get_function_label(nodes) {
return parent.id.name;
}
}
+
+/**
+ * @param {AST.SvelteNode} parent
+ * @param {Context} context
+ */
+function is_variable_declaration(parent, context) {
+ return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
+}
+
+/**
+ * @param {AST.SvelteNode} parent
+ */
+function is_class_property_definition(parent) {
+ return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
+}
+
+/**
+ * @param {AST.SvelteNode} node
+ * @param {Context} context
+ */
+function is_class_property_assignment_at_constructor_root(node, context) {
+ if (
+ node.type === 'AssignmentExpression' &&
+ node.operator === '=' &&
+ node.left.type === 'MemberExpression' &&
+ node.left.object.type === 'ThisExpression' &&
+ ((node.left.property.type === 'Identifier' && !node.left.computed) ||
+ node.left.property.type === 'PrivateIdentifier' ||
+ node.left.property.type === 'Literal')
+ ) {
+ // MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
+ const parent = get_parent(context.path, -5);
+ return parent?.type === 'MethodDefinition' && parent.kind === 'constructor';
+ }
+
+ return false;
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
index 0463e4da85..ffc39ac00d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -1,30 +1,107 @@
-/** @import { ClassBody } from 'estree' */
+/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
+/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
+import * as b from '#compiler/builders';
import { get_rune } from '../../scope.js';
+import * as e from '../../../errors.js';
+import { is_state_creation_rune } from '../../../../utils.js';
+import { get_name } from '../../nodes.js';
+import { regex_invalid_identifier_chars } from '../../patterns.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
- /** @type {{name: string, private: boolean}[]} */
- const derived_state = [];
+ if (!context.state.analysis.runes) {
+ context.next();
+ return;
+ }
+
+ /** @type {string[]} */
+ const private_ids = [];
- for (const definition of node.body) {
+ for (const prop of node.body) {
if (
- definition.type === 'PropertyDefinition' &&
- (definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
- definition.value?.type === 'CallExpression'
+ (prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
+ prop.key.type === 'PrivateIdentifier'
) {
- const rune = get_rune(definition.value, context.state.scope);
- if (rune === '$derived' || rune === '$derived.by') {
- derived_state.push({
- name: definition.key.name,
- private: definition.key.type === 'PrivateIdentifier'
- });
+ private_ids.push(prop.key.name);
+ }
+ }
+
+ /** @type {Map} */
+ const state_fields = new Map();
+
+ context.state.analysis.classes.set(node, state_fields);
+
+ /** @type {MethodDefinition | null} */
+ let constructor = null;
+
+ /**
+ * @param {PropertyDefinition | AssignmentExpression} node
+ * @param {Expression | PrivateIdentifier} key
+ * @param {Expression | null | undefined} value
+ */
+ function handle(node, key, value) {
+ const name = get_name(key);
+ if (name === null) return;
+
+ const rune = get_rune(value, context.state.scope);
+
+ if (rune && is_state_creation_rune(rune)) {
+ if (state_fields.has(name)) {
+ e.state_field_duplicate(node, name);
}
+
+ state_fields.set(name, {
+ node,
+ type: rune,
+ // @ts-expect-error for public state this is filled out in a moment
+ key: key.type === 'PrivateIdentifier' ? key : null,
+ value: /** @type {CallExpression} */ (value)
+ });
+ }
+ }
+
+ for (const child of node.body) {
+ if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
+ handle(child, child.key, child.value);
}
+
+ if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
+ constructor = child;
+ }
+ }
+
+ if (constructor) {
+ for (const statement of constructor.value.body.body) {
+ if (statement.type !== 'ExpressionStatement') continue;
+ if (statement.expression.type !== 'AssignmentExpression') continue;
+
+ const { left, right } = statement.expression;
+
+ if (left.type !== 'MemberExpression') continue;
+ if (left.object.type !== 'ThisExpression') continue;
+ if (left.computed && left.property.type !== 'Literal') continue;
+
+ handle(statement.expression, left.property, right);
+ }
+ }
+
+ for (const [name, field] of state_fields) {
+ if (name[0] === '#') {
+ continue;
+ }
+
+ let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
+ while (private_ids.includes(deconflicted)) {
+ deconflicted = '_' + deconflicted;
+ }
+
+ private_ids.push(deconflicted);
+ field.key = b.private_id(deconflicted);
}
- context.next({ ...context.state, derived_state });
+ context.next({ ...context.state, state_fields });
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
index efbbe6cfa2..89a81127c1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -39,7 +39,7 @@ export function Identifier(node, context) {
if (
is_rune(node.name) &&
context.state.scope.get(node.name) === null &&
- context.state.scope.get(node.name.slice(1)) === null
+ context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub'
) {
/** @type {Expression} */
let current = node;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js
new file mode 100644
index 0000000000..58684ba71c
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js
@@ -0,0 +1,14 @@
+/** @import { Literal } from 'estree' */
+import * as w from '../../../warnings.js';
+import { regex_bidirectional_control_characters } from '../../patterns.js';
+
+/**
+ * @param {Literal} node
+ */
+export function Literal(node) {
+ if (typeof node.value === 'string') {
+ if (regex_bidirectional_control_characters.test(node.value)) {
+ w.bidirectional_control_characters(node);
+ }
+ }
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
new file mode 100644
index 0000000000..99d05cb47c
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
@@ -0,0 +1,21 @@
+/** @import { PropertyDefinition } from 'estree' */
+/** @import { Context } from '../types' */
+import * as e from '../../../errors.js';
+import { get_name } from '../../nodes.js';
+
+/**
+ * @param {PropertyDefinition} node
+ * @param {Context} context
+ */
+export function PropertyDefinition(node, context) {
+ const name = get_name(node.key);
+ const field = name && context.state.state_fields.get(name);
+
+ if (field && node !== field.node && node.value) {
+ if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
+ e.state_field_invalid_assignment(node);
+ }
+ }
+
+ context.next();
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js
new file mode 100644
index 0000000000..978042bbc5
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js
@@ -0,0 +1,12 @@
+/** @import { TemplateElement } from 'estree' */
+import * as w from '../../../warnings.js';
+import { regex_bidirectional_control_characters } from '../../patterns.js';
+
+/**
+ * @param {TemplateElement} node
+ */
+export function TemplateElement(node) {
+ if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
+ w.bidirectional_control_characters(node);
+ }
+}
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 363a111b7d..a03421e8dd 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
@@ -1,20 +1,52 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
-import { regex_not_whitespace } from '../../patterns.js';
+import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
+import * as w from '../../../warnings.js';
+import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
/**
* @param {AST.Text} node
* @param {Context} context
*/
export function Text(node, context) {
- const in_template = context.path.at(-1)?.type === 'Fragment';
+ const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
- if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
+ if (
+ parent.type === 'Fragment' &&
+ context.state.parent_element &&
+ regex_not_whitespace.test(node.data)
+ ) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
+
+ regex_bidirectional_control_characters.lastIndex = 0;
+ for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
+ let is_ignored = false;
+
+ // if we have a svelte-ignore comment earlier in the text, bail
+ // (otherwise we can only use svelte-ignore on parent elements/blocks)
+ if (parent.type === 'Fragment') {
+ for (const child of parent.nodes) {
+ if (child === node) break;
+
+ if (child.type === 'Comment') {
+ is_ignored ||= extract_svelte_ignore(
+ child.start + 4,
+ child.data,
+ context.state.analysis.runes
+ ).includes('bidirectional_control_characters');
+ }
+ }
+ }
+
+ if (!is_ignored) {
+ let start = match.index + node.start;
+ w.bidirectional_control_characters({ start, end: start + match[0].length });
+ }
+ }
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js
index 741effc67a..13f4b9019e 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js
@@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function UpdateExpression(node, context) {
- validate_assignment(node, node.argument, context.state);
+ validate_assignment(node, node.argument, context);
if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
index 04bf3d2ff3..aca87fab81 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
@@ -1,3 +1,4 @@
+/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
import * as e from '../../../../errors.js';
@@ -74,7 +75,8 @@ export function visit_component(node, context) {
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
- attribute.type !== 'BindDirective'
+ attribute.type !== 'BindDirective' &&
+ attribute.type !== 'AttachTag'
) {
e.component_invalid_directive(attribute);
}
@@ -91,15 +93,10 @@ export function visit_component(node, context) {
validate_attribute(attribute, node);
if (is_expression_attribute(attribute)) {
- const expression = get_attribute_expression(attribute);
- if (expression.type === 'SequenceExpression') {
- let i = /** @type {number} */ (expression.start);
- while (--i > 0) {
- const char = context.state.analysis.source[i];
- if (char === '(') break; // parenthesized sequence expressions are ok
- if (char === '{') e.attribute_invalid_sequence_expression(expression);
- }
- }
+ disallow_unparenthesized_sequences(
+ get_attribute_expression(attribute),
+ context.state.analysis.source
+ );
}
}
@@ -113,6 +110,10 @@ export function visit_component(node, context) {
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
+
+ if (attribute.type === 'AttachTag') {
+ disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
+ }
}
// If the component has a slot attribute — ` ` —
@@ -158,3 +159,18 @@ export function visit_component(node, context) {
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}
+
+/**
+ * @param {Expression} expression
+ * @param {string} source
+ */
+function disallow_unparenthesized_sequences(expression, source) {
+ if (expression.type === 'SequenceExpression') {
+ let i = /** @type {number} */ (expression.start);
+ while (--i > 0) {
+ const char = source[i];
+ if (char === '(') break; // parenthesized sequence expressions are ok
+ if (char === '{') e.attribute_invalid_sequence_expression(expression);
+ }
+ }
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
index 12e21c386c..d7b682da08 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
@@ -4,24 +4,25 @@
/** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js';
-import { extract_identifiers } from '../../../../utils/ast.js';
+import { extract_identifiers, get_parent } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
+import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
- * @param {AnalysisState} state
+ * @param {Context} context
*/
-export function validate_assignment(node, argument, state) {
- validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
+export function validate_assignment(node, argument, context) {
+ validate_no_const_assignment(node, argument, context.state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
- const binding = state.scope.get(argument.name);
+ const binding = context.state.scope.get(argument.name);
- if (state.analysis.runes) {
- if (binding?.node === state.analysis.props_id) {
+ if (context.state.analysis.runes) {
+ if (binding?.node === context.state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
@@ -34,6 +35,41 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
+
+ if (argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression') {
+ const name =
+ argument.computed && argument.property.type !== 'Literal'
+ ? null
+ : get_name(argument.property);
+
+ const field = name !== null && context.state.state_fields?.get(name);
+
+ // check we're not assigning to a state field before its declaration in the constructor
+ if (field && field.node.type === 'AssignmentExpression' && node !== field.node) {
+ let i = context.path.length;
+ while (i--) {
+ const parent = context.path[i];
+
+ if (
+ parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression' ||
+ parent.type === 'ArrowFunctionExpression'
+ ) {
+ const grandparent = get_parent(context.path, i - 1);
+
+ if (
+ grandparent.type === 'MethodDefinition' &&
+ grandparent.kind === 'constructor' &&
+ /** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)
+ ) {
+ e.state_field_invalid_assignment(node);
+ }
+
+ break;
+ }
+ }
+ }
+ }
}
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index 3cb8e63469..7c3e7b96d7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -57,6 +57,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
+import { AttachTag } from './visitors/AttachTag.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
@@ -133,6 +134,7 @@ const visitors = {
TransitionDirective,
UpdateExpression,
UseDirective,
+ AttachTag,
VariableDeclaration
};
@@ -164,8 +166,7 @@ export function client_component(analysis, options) {
},
events: new Set(),
preserve_whitespace: options.preserveWhitespace,
- public_state: new Map(),
- private_state: new Map(),
+ state_fields: new Map(),
transform: {},
in_constructor: false,
instance_level_snippets: [],
@@ -698,8 +699,7 @@ export function client_module(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
- public_state: new Map(),
- private_state: new Map(),
+ state_fields: new Map(),
transform: {},
in_constructor: false
};
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 6d2655a5ff..2f814568b3 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
@@ -9,15 +9,12 @@ import type {
UpdateExpression,
VariableDeclaration
} from 'estree';
-import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
+import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState {
- readonly private_state: Map;
- readonly public_state: Map;
-
/**
* `true` if the current lexical scope belongs to a class constructor. this allows
* us to rewrite `this.foo` as `this.#foo.value`
@@ -100,11 +97,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly module_level_snippets: VariableDeclaration[];
}
-export interface StateField {
- kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
- id: PrivateIdentifier;
-}
-
export type Context = import('zimmerframe').Context;
export type Visitors = import('zimmerframe').Visitors;
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 e3f9450050..a46b318601 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
@@ -11,6 +11,8 @@ import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
+import { get_rune } from '../../../scope.js';
+import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression} node
@@ -50,25 +52,42 @@ const callees = {
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {
- // Handle class private/public state assignment cases
- if (
- context.state.analysis.runes &&
- left.type === 'MemberExpression' &&
- left.property.type === 'PrivateIdentifier'
- ) {
- const private_state = context.state.private_state.get(left.property.name);
+ if (context.state.analysis.runes && left.type === 'MemberExpression') {
+ const name = get_name(left.property);
+ const field = name && context.state.state_fields.get(name);
+
+ if (field) {
+ // special case — state declaration in class constructor
+ if (field.node.type === 'AssignmentExpression' && left === field.node.left) {
+ const rune = get_rune(right, context.state.scope);
+
+ if (rune) {
+ const child_state = {
+ ...context.state,
+ in_constructor: rune !== '$derived' && rune !== '$derived.by'
+ };
+
+ return b.assignment(
+ operator,
+ b.member(b.this, field.key),
+ /** @type {Expression} */ (context.visit(right, child_state))
+ );
+ }
+ }
- if (private_state !== undefined) {
- let value = /** @type {Expression} */ (
- context.visit(build_assignment_value(operator, left, right))
- );
+ // special case — assignment to private state field
+ if (left.property.type === 'PrivateIdentifier') {
+ let value = /** @type {Expression} */ (
+ context.visit(build_assignment_value(operator, left, right))
+ );
- const needs_proxy =
- private_state.kind === 'state' &&
- is_non_coercive_operator(operator) &&
- should_proxy(value, context.state.scope);
+ const needs_proxy =
+ field.type === '$state' &&
+ is_non_coercive_operator(operator) &&
+ should_proxy(value, context.state.scope);
- return b.call('$.set', left, value, needs_proxy && b.true);
+ return b.call('$.set', left, value, needs_proxy && b.true);
+ }
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js
new file mode 100644
index 0000000000..062604cacc
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js
@@ -0,0 +1,21 @@
+/** @import { Expression } from 'estree' */
+/** @import { AST } from '#compiler' */
+/** @import { ComponentContext } from '../types' */
+import * as b from '../../../../utils/builders.js';
+
+/**
+ * @param {AST.AttachTag} node
+ * @param {ComponentContext} context
+ */
+export function AttachTag(node, context) {
+ context.state.init.push(
+ b.stmt(
+ b.call(
+ '$.attach',
+ context.state.node,
+ b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
+ )
+ )
+ );
+ context.next();
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index 04b96cb32e..532af08fd1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -4,19 +4,52 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
+import { should_proxy } from '../utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
- switch (get_rune(node, context.state.scope)) {
+ const rune = get_rune(node, context.state.scope);
+
+ switch (rune) {
case '$host':
return b.id('$$props.$$host');
case '$effect.tracking':
return b.call('$.effect_tracking');
+ // transform state field assignments in constructors
+ case '$state':
+ case '$state.raw': {
+ let arg = node.arguments[0];
+
+ /** @type {Expression | undefined} */
+ let value = undefined;
+
+ if (arg) {
+ value = /** @type {Expression} */ (context.visit(node.arguments[0]));
+
+ if (
+ rune === '$state' &&
+ should_proxy(/** @type {Expression} */ (arg), context.state.scope)
+ ) {
+ value = b.call('$.proxy', value);
+ }
+ }
+
+ return b.call('$.state', value);
+ }
+
+ case '$derived':
+ case '$derived.by': {
+ let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
+ if (rune === '$derived') fn = b.thunk(fn);
+
+ return b.call('$.derived', fn);
+ }
+
case '$state.snapshot':
return b.call(
'$.snapshot',
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 f3ebd94069..71a8fc3d05 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
@@ -1,184 +1,96 @@
-/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
-/** @import { Context, StateField } from '../types' */
+/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
+/** @import { StateField } from '#compiler' */
+/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
-import { regex_invalid_identifier_chars } from '../../../patterns.js';
-import { get_rune } from '../../../scope.js';
-import { should_proxy } from '../utils.js';
+import { get_name } from '../../../nodes.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
- if (!context.state.analysis.runes) {
+ const state_fields = context.state.analysis.classes.get(node);
+
+ if (!state_fields) {
+ // in legacy mode, do nothing
context.next();
return;
}
- /** @type {Map} */
- const public_state = new Map();
+ /** @type {Array} */
+ const body = [];
- /** @type {Map} */
- const private_state = new Map();
+ const child_state = { ...context.state, state_fields };
- /** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
- const definition_names = new Map();
+ for (const [name, field] of state_fields) {
+ if (name[0] === '#') {
+ continue;
+ }
- /** @type {string[]} */
- const private_ids = [];
+ // insert backing fields for stuff declared in the constructor
+ if (field.node.type === 'AssignmentExpression') {
+ const member = b.member(b.this, field.key);
- for (const definition of node.body) {
- if (
- (definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
- (definition.key.type === 'Identifier' ||
- definition.key.type === 'PrivateIdentifier' ||
- definition.key.type === 'Literal')
- ) {
- const type = definition.key.type;
- const name = get_name(definition.key, public_state);
- if (!name) continue;
-
- // we store the deconflicted name in the map so that we can access it later
- definition_names.set(definition.key, name);
-
- const is_private = type === 'PrivateIdentifier';
- if (is_private) private_ids.push(name);
-
- if (definition.value?.type === 'CallExpression') {
- const rune = get_rune(definition.value, context.state.scope);
- if (
- rune === '$state' ||
- rune === '$state.raw' ||
- rune === '$derived' ||
- rune === '$derived.by'
- ) {
- /** @type {StateField} */
- const field = {
- kind:
- rune === '$state'
- ? 'state'
- : rune === '$state.raw'
- ? 'raw_state'
- : rune === '$derived.by'
- ? 'derived_by'
- : 'derived',
- // @ts-expect-error this is set in the next pass
- id: is_private ? definition.key : null
- };
-
- if (is_private) {
- private_state.set(name, field);
- } else {
- public_state.set(name, field);
- }
- }
- }
- }
- }
+ const should_proxy = field.type === '$state' && true; // TODO
- // each `foo = $state()` needs a backing `#foo` field
- for (const [name, field] of public_state) {
- let deconflicted = name;
- while (private_ids.includes(deconflicted)) {
- deconflicted = '_' + deconflicted;
- }
+ const key = b.key(name);
- private_ids.push(deconflicted);
- field.id = b.private_id(deconflicted);
- }
+ body.push(
+ b.prop_def(field.key, null),
- /** @type {Array} */
- const body = [];
+ b.method('get', key, [], [b.return(b.call('$.get', member))]),
- const child_state = { ...context.state, public_state, private_state };
+ b.method(
+ 'set',
+ key,
+ [b.id('value')],
+ [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
+ )
+ );
+ }
+ }
// Replace parts of the class body
for (const definition of node.body) {
- if (
- definition.type === 'PropertyDefinition' &&
- (definition.key.type === 'Identifier' ||
- definition.key.type === 'PrivateIdentifier' ||
- definition.key.type === 'Literal')
- ) {
- const name = definition_names.get(definition.key);
- if (!name) continue;
-
- const is_private = definition.key.type === 'PrivateIdentifier';
- const field = (is_private ? private_state : public_state).get(name);
-
- if (definition.value?.type === 'CallExpression' && field !== undefined) {
- let value = null;
-
- if (definition.value.arguments.length > 0) {
- const init = /** @type {Expression} **/ (
- context.visit(definition.value.arguments[0], child_state)
- );
-
- value =
- field.kind === 'state'
- ? b.call(
- '$.state',
- should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
- )
- : field.kind === 'raw_state'
- ? b.call('$.state', init)
- : field.kind === 'derived_by'
- ? b.call('$.derived', init)
- : b.call('$.derived', b.thunk(init));
- } else {
- // if no arguments, we know it's state as `$derived()` is a compile error
- value = b.call('$.state');
- }
-
- if (is_private) {
- body.push(b.prop_def(field.id, value));
- } else {
- // #foo;
- const member = b.member(b.this, field.id);
- body.push(b.prop_def(field.id, value));
-
- // get foo() { return this.#foo; }
- body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
-
- // set foo(value) { this.#foo = value; }
- const val = b.id('value');
-
- body.push(
- b.method(
- 'set',
- definition.key,
- [val],
- [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
- )
- );
- }
- continue;
- }
+ if (definition.type !== 'PropertyDefinition') {
+ body.push(
+ /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
+ );
+ continue;
}
- body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
- }
+ const name = get_name(definition.key);
+ const field = name && /** @type {StateField} */ (state_fields.get(name));
- return { ...node, body };
-}
+ if (!field) {
+ body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
+ continue;
+ }
-/**
- * @param {Identifier | PrivateIdentifier | Literal} node
- * @param {Map} public_state
- */
-function get_name(node, public_state) {
- if (node.type === 'Literal') {
- let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
-
- // the above could generate conflicts because it has to generate a valid identifier
- // so stuff like `0` and `1` or `state%` and `state^` will result in the same string
- // so we have to de-conflict. We can only check `public_state` because private state
- // can't have literal keys
- while (name && public_state.has(name)) {
- name = '_' + name;
+ if (name[0] === '#') {
+ body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
+ } else if (field.node === definition) {
+ const member = b.member(b.this, field.key);
+
+ const should_proxy = field.type === '$state' && true; // TODO
+
+ body.push(
+ b.prop_def(
+ field.key,
+ /** @type {CallExpression} */ (context.visit(field.value, child_state))
+ ),
+
+ b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),
+
+ b.method(
+ 'set',
+ definition.key,
+ [b.id('value')],
+ [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
+ )
+ );
}
- return name;
- } else {
- return node.name;
}
+
+ return { ...node, body };
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
index ab88345ddd..7e89b0e705 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
@@ -9,9 +9,11 @@ import * as b from '#compiler/builders';
export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
- const field = context.state.private_state.get(node.property.name);
+ const field = context.state.state_fields.get('#' + node.property.name);
+
if (field) {
- return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
+ return context.state.in_constructor &&
+ (field.type === '$state.raw' || field.type === '$state')
? b.member(node, 'v')
: b.call('$.get', node);
}
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 e4159bf3e9..e8881cc450 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
@@ -83,7 +83,7 @@ export function RegularElement(node, context) {
/** @type {AST.StyleDirective[]} */
const style_directives = [];
- /** @type {Array} */
+ /** @type {Array} */
const other_directives = [];
/** @type {ExpressionStatement[]} */
@@ -153,6 +153,10 @@ export function RegularElement(node, context) {
has_use = true;
other_directives.push(attribute);
break;
+
+ case 'AttachTag':
+ other_directives.push(attribute);
+ break;
}
}
@@ -582,7 +586,7 @@ export function build_style_directives_object(style_directives, context) {
/**
* Serializes an assignment to an element property by adding relevant statements to either only
- * the init or the the init and update arrays, depending on whether or not the value is dynamic.
+ * the init or the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* element.property = value;
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 96be119b84..66952eb298 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
@@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' &&
- context.state.private_state.has(argument.property.name)
+ context.state.state_fields.has('#' + argument.property.name)
) {
let fn = '$.update';
if (node.prefix) fn += '_pre';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index 2c6fc5bcac..851faeccfb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -2,7 +2,7 @@
/** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
-import { extract_paths } from '../../../../utils/ast.js';
+import { build_pattern, extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
@@ -141,20 +141,20 @@ export function VariableDeclaration(node, context) {
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
);
} else {
- const tmp = context.state.scope.generate('tmp');
- const paths = extract_paths(declarator.id);
+ const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
declarations.push(
- b.declarator(b.id(tmp), value),
- ...paths.map((path) => {
- const value = path.expression?.(b.id(tmp));
- const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
- return b.declarator(
- path.node,
- binding?.kind === 'state' || binding?.kind === 'raw_state'
- ? create_state_declarator(binding.node, value)
- : value
- );
- })
+ b.declarator(pattern, value),
+ .../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
+ ([original, replacement]) => {
+ const binding = context.state.scope.get(original.name);
+ return b.declarator(
+ original,
+ binding?.kind === 'state' || binding?.kind === 'raw_state'
+ ? create_state_declarator(binding.node, replacement)
+ : replacement
+ );
+ }
+ )
);
}
@@ -196,8 +196,7 @@ export function VariableDeclaration(node, context) {
);
}
} else {
- const bindings = extract_paths(declarator.id);
-
+ const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const init = /** @type {CallExpression} */ (declarator.init);
/** @type {Identifier} */
@@ -215,10 +214,16 @@ export function VariableDeclaration(node, context) {
);
}
- for (let i = 0; i < bindings.length; i++) {
- const binding = bindings[i];
+ for (let i = 0; i < replacements.size; i++) {
+ const [original, replacement] = [...replacements][i];
declarations.push(
- b.declarator(binding.node, b.call('$.derived', b.thunk(binding.expression(rhs))))
+ b.declarator(
+ original,
+ b.call(
+ '$.derived',
+ b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)]))
+ )
+ )
);
}
}
@@ -330,19 +335,19 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
];
}
- const tmp = scope.generate('tmp');
- const paths = extract_paths(declarator.id);
+ const [pattern, replacements] = build_pattern(declarator.id, scope);
return [
- b.declarator(b.id(tmp), value),
- ...paths.map((path) => {
- const value = path.expression?.(b.id(tmp));
- const binding = scope.get(/** @type {Identifier} */ (path.node).name);
- return b.declarator(
- path.node,
- binding?.kind === 'state'
- ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
- : value
- );
- })
+ b.declarator(pattern, value),
+ .../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
+ ([original, replacement]) => {
+ const binding = scope.get(original.name);
+ return b.declarator(
+ original,
+ binding?.kind === 'state'
+ ? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined)
+ : replacement
+ );
+ }
+ )
];
}
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 7c0fda5129..4e0a239a55 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
@@ -281,6 +281,14 @@ export function build_component(node, component_name, context, anchor = context.
);
}
}
+ } else if (attribute.type === 'AttachTag') {
+ let expression = /** @type {Expression} */ (context.visit(attribute.expression));
+
+ if (attribute.metadata.expression.has_state) {
+ expression = b.arrow([b.id('$$node')], b.call(expression, b.id('$$node')));
+ }
+
+ push_prop(b.prop('get', b.call('$.attachment'), expression, true));
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
index 88f1c2ea17..fa67bfe3e1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
@@ -86,7 +86,7 @@ export function build_template_chunk(
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
if (evaluated.is_known) {
- value = b.literal(evaluated.value);
+ value = b.literal((evaluated.value ?? '') + '');
}
return { value, has_state };
@@ -105,7 +105,7 @@ export function build_template_chunk(
}
if (evaluated.is_known) {
- quasi.value.cooked += evaluated.value + '';
+ quasi.value.cooked += (evaluated.value ?? '') + '';
} else {
if (!evaluated.is_defined) {
// add `?? ''` where necessary
@@ -342,15 +342,18 @@ export function validate_mutation(node, context, expression) {
const state = /** @type {ComponentClientTransformState} */ (context.state);
state.analysis.needs_mutation_validation = true;
- /** @type {Array} */
+ /** @type {Array} */
const path = [];
while (left.type === 'MemberExpression') {
if (left.property.type === 'Literal') {
path.unshift(left.property);
} else if (left.property.type === 'Identifier') {
+ const transform = Object.hasOwn(context.state.transform, left.property.name)
+ ? context.state.transform[left.property.name]
+ : null;
if (left.computed) {
- path.unshift(left.property);
+ path.unshift(transform?.read ? transform.read(left.property) : left.property);
} else {
path.unshift(b.literal(left.property.name));
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 9ebd8f03c4..f24b4476d4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -24,7 +24,6 @@ import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
-import { MemberExpression } from './visitors/MemberExpression.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@@ -51,7 +50,6 @@ const global_visitors = {
ExpressionStatement,
Identifier,
LabeledStatement,
- MemberExpression,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@@ -101,7 +99,7 @@ export function server_component(analysis, options) {
template: /** @type {any} */ (null),
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
- private_derived: new Map(),
+ state_fields: new Map(),
skip_hydration_boundaries: false
};
@@ -397,7 +395,7 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map(),
- private_derived: new Map()
+ state_fields: new Map()
};
const module = /** @type {Program} */ (
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
index 971271642c..adde7480cb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
@@ -2,12 +2,10 @@ import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
-import type { StateField } from '../client/types.js';
export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map;
- readonly private_derived: Map;
}
export interface ComponentServerTransformState extends ServerTransformState {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
index 071a12f9bc..280c16dbd2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
@@ -3,6 +3,8 @@
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js';
+import { get_name } from '../../../nodes.js';
+import { get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
/**
@@ -22,6 +24,29 @@ export function AssignmentExpression(node, context) {
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {
+ if (context.state.analysis.runes && left.type === 'MemberExpression') {
+ const name = get_name(left.property);
+ const field = name && context.state.state_fields.get(name);
+
+ // special case — state declaration in class constructor
+ if (field && field.node.type === 'AssignmentExpression' && left === field.node.left) {
+ const rune = get_rune(right, context.state.scope);
+
+ if (rune) {
+ const key =
+ left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw'
+ ? left.property
+ : field.key;
+
+ return b.assignment(
+ operator,
+ b.member(b.this, key, key.type === 'Literal'),
+ /** @type {Expression} */ (context.visit(right))
+ );
+ }
+ }
+ }
+
let object = left;
while (object.type === 'MemberExpression') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
index c9fa5a38c7..6e7258b5b3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
@@ -29,6 +29,15 @@ export function CallExpression(node, context) {
return b.false;
}
+ if (rune === '$state' || rune === '$state.raw') {
+ return node.arguments[0] ? context.visit(node.arguments[0]) : b.void0;
+ }
+
+ if (rune === '$derived' || rune === '$derived.by') {
+ const fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
+ return b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn);
+ }
+
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
index c0ebdeae08..6797b0beff 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
@@ -1,120 +1,77 @@
-/** @import { ClassBody, Expression, MethodDefinition, PropertyDefinition } from 'estree' */
+/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { Context } from '../types.js' */
-/** @import { StateField } from '../../client/types.js' */
-import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
-import { get_rune } from '../../../scope.js';
+import { get_name } from '../../../nodes.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
- if (!context.state.analysis.runes) {
+ const state_fields = context.state.analysis.classes.get(node);
+
+ if (!state_fields) {
+ // in legacy mode, do nothing
context.next();
return;
}
- /** @type {Map} */
- const public_derived = new Map();
+ /** @type {Array} */
+ const body = [];
- /** @type {Map} */
- const private_derived = new Map();
+ const child_state = { ...context.state, state_fields };
- /** @type {string[]} */
- const private_ids = [];
+ for (const [name, field] of state_fields) {
+ if (name[0] === '#') {
+ continue;
+ }
- for (const definition of node.body) {
+ // insert backing fields for stuff declared in the constructor
if (
- definition.type === 'PropertyDefinition' &&
- (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
+ field &&
+ field.node.type === 'AssignmentExpression' &&
+ (field.type === '$derived' || field.type === '$derived.by')
) {
- const { type, name } = definition.key;
-
- const is_private = type === 'PrivateIdentifier';
- if (is_private) private_ids.push(name);
+ const member = b.member(b.this, field.key);
- if (definition.value?.type === 'CallExpression') {
- const rune = get_rune(definition.value, context.state.scope);
- if (rune === '$derived' || rune === '$derived.by') {
- /** @type {StateField} */
- const field = {
- kind: rune === '$derived.by' ? 'derived_by' : 'derived',
- // @ts-expect-error this is set in the next pass
- id: is_private ? definition.key : null
- };
-
- if (is_private) {
- private_derived.set(name, field);
- } else {
- public_derived.set(name, field);
- }
- }
- }
+ body.push(
+ b.prop_def(field.key, null),
+ b.method('get', b.key(name), [], [b.return(b.call(member))])
+ );
}
}
- // each `foo = $derived()` needs a backing `#foo` field
- for (const [name, field] of public_derived) {
- let deconflicted = name;
- while (private_ids.includes(deconflicted)) {
- deconflicted = '_' + deconflicted;
- }
-
- private_ids.push(deconflicted);
- field.id = b.private_id(deconflicted);
- }
-
- /** @type {Array} */
- const body = [];
-
- const child_state = { ...context.state, private_derived };
-
// Replace parts of the class body
for (const definition of node.body) {
- if (
- definition.type === 'PropertyDefinition' &&
- (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
- ) {
- const name = definition.key.name;
-
- const is_private = definition.key.type === 'PrivateIdentifier';
- const field = (is_private ? private_derived : public_derived).get(name);
+ if (definition.type !== 'PropertyDefinition') {
+ body.push(
+ /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
+ );
+ continue;
+ }
- if (definition.value?.type === 'CallExpression' && field !== undefined) {
- const init = /** @type {Expression} **/ (
- context.visit(definition.value.arguments[0], child_state)
- );
- const value =
- field.kind === 'derived_by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
+ const name = get_name(definition.key);
+ const field = name && state_fields.get(name);
- if (is_private) {
- body.push(b.prop_def(field.id, value));
- } else {
- // #foo;
- const member = b.member(b.this, field.id);
- body.push(b.prop_def(field.id, value));
+ if (!field) {
+ body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
+ continue;
+ }
- // get foo() { return this.#foo; }
- body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
+ if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') {
+ body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
+ } else if (field.node === definition) {
+ const member = b.member(b.this, field.key);
- if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
- body.push(
- b.method(
- 'set',
- definition.key,
- [b.id('_')],
- [b.throw_error(`Cannot update a derived property ('${name}')`)]
- )
- );
- }
- }
+ body.push(
+ b.prop_def(
+ field.key,
+ /** @type {CallExpression} */ (context.visit(field.value, child_state))
+ ),
- continue;
- }
+ b.method('get', definition.key, [], [b.return(b.call(member))])
+ );
}
-
- body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
return { ...node, body };
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
deleted file mode 100644
index 73631395e6..0000000000
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/** @import { MemberExpression } from 'estree' */
-/** @import { Context } from '../types.js' */
-import * as b from '#compiler/builders';
-
-/**
- * @param {MemberExpression} node
- * @param {Context} context
- */
-export function MemberExpression(node, context) {
- if (
- context.state.analysis.runes &&
- node.object.type === 'ThisExpression' &&
- node.property.type === 'PrivateIdentifier'
- ) {
- const field = context.state.private_derived.get(node.property.name);
-
- if (field) {
- return b.call(node);
- }
- }
-
- context.next();
-}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
index 1f2bd3e2b1..17bf516a22 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
@@ -1,8 +1,9 @@
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types.js' */
+/** @import { ComponentAnalysis } from '../../../types.js' */
/** @import { Scope } from '../../../scope.js' */
-import { build_fallback, extract_paths } from '../../../../utils/ast.js';
+import { build_pattern, build_fallback, extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
@@ -50,20 +51,26 @@ export function VariableDeclaration(node, context) {
}
}
});
+
+ // if `$$slots` is declared separately, deconflict
+ const slots_name = /** @type {ComponentAnalysis} */ (context.state.analysis).uses_slots
+ ? b.id('$$slots_')
+ : b.id('$$slots');
+
if (id.type === 'ObjectPattern' && has_rest) {
// If a rest pattern is used within an object pattern, we need to ensure we don't expose $$slots or $$events
id.properties.splice(
id.properties.length - 1,
0,
// @ts-ignore
- b.prop('init', b.id('$$slots'), b.id('$$slots')),
+ b.prop('init', b.id('$$slots'), slots_name),
b.prop('init', b.id('$$events'), b.id('$$events'))
);
} else if (id.type === 'Identifier') {
// If $props is referenced as an identifier, we need to ensure we don't expose $$slots or $$events as properties
// on the identifier reference
id = b.object_pattern([
- b.prop('init', b.id('$$slots'), b.id('$$slots')),
+ b.prop('init', b.id('$$slots'), slots_name),
b.prop('init', b.id('$$events'), b.id('$$events')),
b.rest(b.id(id.name))
]);
@@ -181,13 +188,10 @@ function create_state_declarators(declarator, scope, value) {
return [b.declarator(declarator.id, value)];
}
- const tmp = scope.generate('tmp');
- const paths = extract_paths(declarator.id);
+ const [pattern, replacements] = build_pattern(declarator.id, scope);
return [
- b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below
- ...paths.map((path) => {
- const value = path.expression?.(b.id(tmp));
- return b.declarator(path.node, value);
- })
+ b.declarator(pattern, value),
+ // TODO inject declarator for opts, so we can use it below
+ ...[...replacements].map(([original, replacement]) => b.declarator(original, replacement))
];
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
index 3e6bb0c4c6..85b0869a15 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
@@ -1,7 +1,7 @@
-/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */
+/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Node, Pattern } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
-import { extract_paths, is_expression_async } from '../../../utils/ast.js';
+import { build_pattern, is_expression_async } from '../../../utils/ast.js';
import * as b from '#compiler/builders';
/**
@@ -23,21 +23,23 @@ export function visit_assignment_expression(node, context, build_assignment) {
let changed = false;
- const assignments = extract_paths(node.left).map((path) => {
- const value = path.expression?.(rhs);
+ const [pattern, replacements] = build_pattern(node.left, context.state.scope);
- let assignment = build_assignment('=', path.node, value, context);
- if (assignment !== null) changed = true;
-
- return (
- assignment ??
- b.assignment(
- '=',
- /** @type {Pattern} */ (context.visit(path.node)),
- /** @type {Expression} */ (context.visit(value))
- )
- );
- });
+ const assignments = [
+ b.let(pattern, rhs),
+ ...[...replacements].map(([original, replacement]) => {
+ let assignment = build_assignment(node.operator, original, replacement, context);
+ if (assignment !== null) changed = true;
+ return b.stmt(
+ assignment ??
+ b.assignment(
+ node.operator,
+ /** @type {Identifier} */ (context.visit(original)),
+ /** @type {Expression} */ (context.visit(replacement))
+ )
+ );
+ })
+ ];
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
@@ -45,25 +47,36 @@ export function visit_assignment_expression(node, context, build_assignment) {
}
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
- const sequence = b.sequence(assignments);
+ const block = b.block(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
- sequence.expressions.push(rhs);
+ block.body.push(b.return(rhs));
}
- if (should_cache) {
- // the right hand side is a complex expression, wrap in an IIFE to cache it
- const iife = b.arrow([rhs], sequence);
+ if (is_standalone && !should_cache) {
+ return block;
+ }
- const iife_is_async =
- is_expression_async(value) ||
- assignments.some((assignment) => is_expression_async(assignment));
+ const iife = b.arrow(should_cache ? [rhs] : [], block);
- return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
- }
+ const iife_is_async =
+ is_expression_async(value) ||
+ assignments.some(
+ (assignment) =>
+ (assignment.type === 'ExpressionStatement' &&
+ is_expression_async(assignment.expression)) ||
+ (assignment.type === 'VariableDeclaration' &&
+ assignment.declarations.some(
+ (declaration) =>
+ is_expression_async(declaration.id) ||
+ (declaration.init && is_expression_async(declaration.init))
+ ))
+ );
- return sequence;
+ return iife_is_async
+ ? b.await(b.call(b.async(iife), should_cache ? value : undefined))
+ : b.call(iife, should_cache ? value : undefined);
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
index 5d860207dd..887ad447b9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
@@ -1,5 +1,5 @@
import type { Scope } from '../scope.js';
-import type { AST, ValidatedModuleCompileOptions } from '#compiler';
+import type { AST, StateField, ValidatedModuleCompileOptions } from '#compiler';
import type { Analysis } from '../types.js';
export interface TransformState {
@@ -7,4 +7,6 @@ export interface TransformState {
readonly options: ValidatedModuleCompileOptions;
readonly scope: Scope;
readonly scopes: Map;
+
+ readonly state_fields: Map;
}
diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js
index d342156e1e..faf11f373d 100644
--- a/packages/svelte/src/compiler/phases/nodes.js
+++ b/packages/svelte/src/compiler/phases/nodes.js
@@ -1,4 +1,6 @@
+/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
+
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
*/
@@ -65,3 +67,14 @@ export function create_expression_metadata() {
has_await: false
};
}
+
+/**
+ * @param {Expression | PrivateIdentifier} node
+ */
+export function get_name(node) {
+ if (node.type === 'Literal') return String(node.value);
+ if (node.type === 'PrivateIdentifier') return '#' + node.name;
+ if (node.type === 'Identifier') return node.name;
+
+ return null;
+}
diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js
index bda299de9e..2bee717131 100644
--- a/packages/svelte/src/compiler/phases/patterns.js
+++ b/packages/svelte/src/compiler/phases/patterns.js
@@ -21,3 +21,5 @@ export const regex_invalid_identifier_chars = /(^[^a-zA-Z_$]|[^a-zA-Z0-9_$])/g;
export const regex_starts_with_vowel = /^[aeiou]/;
export const regex_heading_tags = /^h[1-6]$/;
export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%?!|()[\]{}^*+~;]/;
+export const regex_bidirectional_control_characters =
+ /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g;
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 5679fe255f..3179101608 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -23,12 +23,12 @@ export const STRING = Symbol('string');
/** @type {Record} */
const globals = {
- BigInt: [NUMBER, BigInt],
+ BigInt: [NUMBER],
'Math.min': [NUMBER, Math.min],
'Math.max': [NUMBER, Math.max],
'Math.random': [NUMBER],
'Math.floor': [NUMBER, Math.floor],
- // @ts-expect-error
+ // @ts-ignore
'Math.f16round': [NUMBER, Math.f16round],
'Math.round': [NUMBER, Math.round],
'Math.abs': [NUMBER, Math.abs],
@@ -630,9 +630,10 @@ export class Scope {
/**
* @param {string} preferred_name
+ * @param {(name: string, counter: number) => string} [generator]
* @returns {string}
*/
- generate(preferred_name) {
+ generate(preferred_name, generator = (name, counter) => `${name}_${counter}`) {
if (this.#porous) {
return /** @type {Scope} */ (this.parent).generate(preferred_name);
}
@@ -647,7 +648,7 @@ export class Scope {
this.root.conflicts.has(name) ||
is_reserved(name)
) {
- name = `${preferred_name}_${n++}`;
+ name = generator(preferred_name, n++);
}
this.references.set(name, []);
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index 89ff943486..0b61f21b10 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -1,7 +1,8 @@
-import type { AST, Binding } from '#compiler';
+import type { AST, Binding, StateField } from '#compiler';
import type {
AwaitExpression,
CallExpression,
+ ClassBody,
Identifier,
LabeledStatement,
Node,
@@ -37,6 +38,8 @@ export interface Analysis {
immutable: boolean;
tracing: boolean;
+ classes: Map>;
+
// TODO figure out if we can move this to ComponentAnalysis
accessors: boolean;
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index 4d50c2db8a..22b398d9f9 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -2,6 +2,13 @@ import type { SourceMap } from 'magic-string';
import type { Binding } from '../phases/scope.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
+import type { StateCreationRuneName } from '../../utils.js';
+import type {
+ AssignmentExpression,
+ CallExpression,
+ PrivateIdentifier,
+ PropertyDefinition
+} from 'estree';
/** The return value of `compile` from `svelte/compiler` */
export interface CompileResult {
@@ -276,6 +283,13 @@ export interface ExpressionMetadata {
has_await: boolean;
}
+export interface StateField {
+ type: StateCreationRuneName;
+ node: PropertyDefinition | AssignmentExpression;
+ key: PrivateIdentifier;
+ value: CallExpression;
+}
+
export * from './template.js';
export { Binding, Scope } from '../phases/scope.js';
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index fd450bca85..62a8a37a98 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -178,6 +178,16 @@ export namespace AST {
};
}
+ /** A `{@attach foo(...)} tag */
+ export interface AttachTag extends BaseNode {
+ type: 'AttachTag';
+ expression: Expression;
+ /** @internal */
+ metadata: {
+ expression: ExpressionMetadata;
+ };
+ }
+
/** An `animate:` directive */
export interface AnimateDirective extends BaseNode {
type: 'AnimateDirective';
@@ -277,7 +287,7 @@ export namespace AST {
interface BaseElement extends BaseNode {
name: string;
- attributes: Array;
+ attributes: Array;
fragment: Fragment;
}
@@ -471,6 +481,7 @@ export namespace AST {
type: 'SnippetBlock';
expression: Identifier;
parameters: Pattern[];
+ typeParams?: string;
body: Fragment;
/** @internal */
metadata: {
@@ -549,7 +560,13 @@ export namespace AST {
| 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
@@ -559,6 +576,7 @@ export namespace AST {
| AST.Attribute
| AST.SpreadAttribute
| Directive
+ | AST.AttachTag
| AST.Comment
| Block;
diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index 108f4eff64..32ff5a37b3 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -1,7 +1,8 @@
-/** @import { AST } from '#compiler' */
+/** @import { AST, Scope } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
+import is_reference from 'is-reference';
/**
* Gets the left-most identifier of a member expression or identifier.
@@ -128,6 +129,49 @@ export function unwrap_pattern(pattern, nodes = []) {
return nodes;
}
+/**
+ * @param {ESTree.Pattern} id
+ * @param {Scope} scope
+ * @returns {[ESTree.Pattern, Map]}
+ */
+export function build_pattern(id, scope) {
+ /** @type {Map} */
+ const map = new Map();
+
+ /** @type {Map} */
+ const names = new Map();
+
+ let counter = 0;
+
+ for (const node of unwrap_pattern(id)) {
+ const name = scope.generate(`$$${++counter}`, (_, counter) => `$$${counter}`);
+
+ map.set(node, b.id(name));
+
+ if (node.type === 'Identifier') {
+ names.set(node.name, name);
+ }
+ }
+
+ const pattern = walk(id, null, {
+ Identifier(node, context) {
+ if (is_reference(node, /** @type {ESTree.Pattern} */ (context.path.at(-1)))) {
+ const name = names.get(node.name);
+ if (name) return b.id(name);
+ }
+ },
+
+ MemberExpression(node, context) {
+ const n = map.get(node);
+ if (n) return n;
+
+ context.next();
+ }
+ });
+
+ return [pattern, map];
+}
+
/**
* Extracts all identifiers from a pattern.
* @param {ESTree.Pattern} pattern
@@ -580,5 +624,7 @@ export function build_assignment_value(operator, left, right) {
return operator === '='
? right
: // turn something like x += 1 into x = x + 1
- b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
+ ['||=', '&&=', '??='].includes(operator)
+ ? b.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
+ : b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}
diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js
index e6fc8caba5..2a8316308c 100644
--- a/packages/svelte/src/compiler/warnings.js
+++ b/packages/svelte/src/compiler/warnings.js
@@ -86,6 +86,7 @@ export const codes = [
'a11y_role_supports_aria_props_implicit',
'a11y_unknown_aria_attribute',
'a11y_unknown_role',
+ 'bidirectional_control_characters',
'legacy_code',
'unknown_code',
'options_deprecated_accessors',
@@ -113,6 +114,7 @@ export const codes = [
'bind_invalid_each_rest',
'block_empty',
'component_name_lowercase',
+ 'element_implicitly_closed',
'element_invalid_self_closing_tag',
'event_directive_deprecated',
'node_invalid_placement_ssr',
@@ -506,6 +508,14 @@ export function a11y_unknown_role(node, role, suggestion) {
w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
}
+/**
+ * A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences
+ * @param {null | NodeLike} node
+ */
+export function bidirectional_control_characters(node) {
+ w(node, 'bidirectional_control_characters', `A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences\nhttps://svelte.dev/e/bidirectional_control_characters`);
+}
+
/**
* `%code%` is no longer valid — please use `%suggestion%` instead
* @param {null | NodeLike} node
@@ -737,6 +747,16 @@ export function component_name_lowercase(node, name) {
w(node, 'component_name_lowercase', `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter\nhttps://svelte.dev/e/component_name_lowercase`);
}
+/**
+ * This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises.
+ * @param {null | NodeLike} node
+ * @param {string} tag
+ * @param {string} closing
+ */
+export function element_implicitly_closed(node, tag, closing) {
+ w(node, 'element_implicitly_closed', `This element is implicitly closed by the following \`${tag}\`, which can cause an unexpected DOM structure. Add an explicit \`${closing}\` to avoid surprises.\nhttps://svelte.dev/e/element_implicitly_closed`);
+}
+
/**
* Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>%name%>` rather than `<%name% ... />`
* @param {null | NodeLike} node
diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js
index f700901f77..97a48cf596 100644
--- a/packages/svelte/src/constants.js
+++ b/packages/svelte/src/constants.js
@@ -56,3 +56,5 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
* TODO this is currently unused
*/
export const ELEMENTS_WITHOUT_TEXT = ['audio', 'datalist', 'dl', 'optgroup', 'select', 'video'];
+
+export const ATTACHMENT_KEY = '@attach';
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 2dfd657e34..b5590a8553 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -13,6 +13,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
+ read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@@ -204,7 +205,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
let mismatch = false;
if (hydrating) {
- var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
+ var is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 9a2857e9f8..a4a5b68b57 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -5,6 +5,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
+ read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@@ -100,7 +101,7 @@ export function if_block(node, fn, elseif = false) {
let mismatch = false;
if (hydrating) {
- const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
+ const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js
new file mode 100644
index 0000000000..4fc1280138
--- /dev/null
+++ b/packages/svelte/src/internal/client/dom/elements/attachments.js
@@ -0,0 +1,33 @@
+/** @import { Effect } from '#client' */
+import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js';
+
+// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by
+// getting rid of the block/branch stuff and just letting the effect rip.
+// see https://github.com/sveltejs/svelte/pull/15962
+
+/**
+ * @param {Element} node
+ * @param {() => (node: Element) => void} get_fn
+ */
+export function attach(node, get_fn) {
+ /** @type {false | undefined | ((node: Element) => void)} */
+ var fn = undefined;
+
+ /** @type {Effect | null} */
+ var e;
+
+ block(() => {
+ if (fn !== (fn = get_fn())) {
+ if (e) {
+ destroy_effect(e);
+ e = null;
+ }
+
+ if (fn) {
+ e = branch(() => {
+ effect(() => /** @type {(node: Element) => void} */ (fn)(node));
+ });
+ }
+ }
+ });
+}
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index f63f55cc6e..3d1acbd31c 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -13,10 +13,11 @@ import {
set_active_effect,
set_active_reaction
} from '../../runtime.js';
+import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
-import { NAMESPACE_HTML } from '../../../../constants.js';
+import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
@@ -446,6 +447,12 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
set_hydrating(true);
}
+ for (let symbol of Object.getOwnPropertySymbols(next)) {
+ if (symbol.description === ATTACHMENT_KEY) {
+ attach(element, () => next[symbol]);
+ }
+ }
+
return current;
}
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
index 30a8dac1af..f96ee05989 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
@@ -62,7 +62,23 @@ export function bind_current_time(media, get, set = get) {
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_buffered(media, set) {
- listen(media, ['loadedmetadata', 'progress'], () => set(time_ranges_to_array(media.buffered)));
+ /** @type {{ start: number; end: number; }[]} */
+ var current;
+
+ // `buffered` can update without emitting any event, so we check it on various events.
+ // By specs, `buffered` always returns a new object, so we have to compare deeply.
+ listen(media, ['loadedmetadata', 'progress', 'timeupdate', 'seeking'], () => {
+ var ranges = media.buffered;
+
+ if (
+ !current ||
+ current.length !== ranges.length ||
+ current.some((range, i) => ranges.start(i) !== range.start || ranges.end(i) !== range.end)
+ ) {
+ current = time_ranges_to_array(ranges);
+ set(current);
+ }
+ });
}
/**
diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js
index fc081b8956..038ce33f3e 100644
--- a/packages/svelte/src/internal/client/dom/elements/class.js
+++ b/packages/svelte/src/internal/client/dom/elements/class.js
@@ -24,7 +24,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
- // we should only remove the class if the the value is nullish
+ // we should only remove the class if the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index 3374fe713f..c2b7fc7d83 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -26,12 +26,8 @@ export const root_event_handles = new Set();
export function replay_events(dom) {
if (!hydrating) return;
- if (dom.onload) {
- dom.removeAttribute('onload');
- }
- if (dom.onerror) {
- dom.removeAttribute('onerror');
- }
+ dom.removeAttribute('onload');
+ dom.removeAttribute('onerror');
// @ts-expect-error
const event = dom.__e;
if (event !== undefined) {
diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js
index 8523ff97d5..ab3256da82 100644
--- a/packages/svelte/src/internal/client/dom/hydration.js
+++ b/packages/svelte/src/internal/client/dom/hydration.js
@@ -103,3 +103,16 @@ export function remove_nodes() {
node = next;
}
}
+
+/**
+ *
+ * @param {TemplateNode} node
+ */
+export function read_hydration_instruction(node) {
+ if (!node || node.nodeType !== 8) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+
+ return /** @type {Comment} */ (node).data;
+}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index d03ba5fe82..2cf0ba3056 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -1,3 +1,4 @@
+export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
@@ -23,6 +24,7 @@ export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { append_styles } from './dom/css.js';
export { action } from './dom/elements/actions.js';
+export { attach } from './dom/elements/attachments.js';
export {
remove_input_defaults,
set_attribute,
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 8bfd8f9e25..77c58720e1 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -218,9 +218,15 @@ const spread_props_handler = {
for (let p of target.props) {
if (is_function(p)) p = p();
+ if (!p) continue;
+
for (const key in p) {
if (!keys.includes(key)) keys.push(key);
}
+
+ for (const key of Object.getOwnPropertySymbols(p)) {
+ if (!keys.includes(key)) keys.push(key);
+ }
}
return keys;
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index eed6550b93..5a819e1c84 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -855,22 +855,25 @@ export function flushSync(fn) {
if (fn) {
is_flushing = true;
flush_queued_root_effects();
+
+ is_flushing = true;
result = fn();
}
- flush_tasks();
+ while (true) {
+ flush_tasks();
+
+ if (queued_root_effects.length === 0) {
+ if (batch === current_batch) {
+ batch.flush();
+ }
+
+ return /** @type {T} */ (result);
+ }
- while (queued_root_effects.length > 0) {
is_flushing = true;
flush_queued_root_effects();
- flush_tasks();
}
-
- if (batch === current_batch) {
- batch.flush();
- }
-
- return /** @type {T} */ (result);
}
/**
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 22310df18d..d286709719 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -3,6 +3,19 @@ import { ReactiveValue } from './reactive-value.js';
const parenthesis_regex = /\(.+\)/;
+// these keywords are valid media queries but they need to be without parenthesis
+//
+// eg: new MediaQuery('screen')
+//
+// however because of the auto-parenthesis logic in the constructor since there's no parentehesis
+// in the media query they'll be surrounded by parenthesis
+//
+// however we can check if the media query is only composed of these keywords
+// and skip the auto-parenthesis
+//
+// https://github.com/sveltejs/svelte/issues/15930
+const non_parenthesized_keywords = new Set(['all', 'print', 'screen', 'and', 'or', 'not', 'only']);
+
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*
@@ -27,7 +40,12 @@ export class MediaQuery extends ReactiveValue {
* @param {boolean} [fallback] Fallback value for the server
*/
constructor(query, fallback) {
- let final_query = parenthesis_regex.test(query) ? query : `(${query})`;
+ let final_query =
+ parenthesis_regex.test(query) ||
+ // we need to use `some` here because technically this `window.matchMedia('random,screen')` still returns true
+ query.split(/[\s,]+/).some((keyword) => non_parenthesized_keywords.has(keyword.trim()))
+ ? query
+ : `(${query})`;
const q = window.matchMedia(final_query);
super(
() => q.matches,
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 90d61d0d5f..cd3a9ac61a 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -428,15 +428,19 @@ export function is_mathml(name) {
return MATHML_ELEMENTS.includes(name);
}
-const RUNES = /** @type {const} */ ([
+export const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state',
'$state.raw',
+ '$derived',
+ '$derived.by'
+]);
+
+const RUNES = /** @type {const} */ ([
+ ...STATE_CREATION_RUNES,
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
- '$derived',
- '$derived.by',
'$effect',
'$effect.pre',
'$effect.tracking',
@@ -448,12 +452,24 @@ const RUNES = /** @type {const} */ ([
'$host'
]);
+/** @typedef {RUNES[number]} RuneName */
+
/**
* @param {string} name
- * @returns {name is RUNES[number]}
+ * @returns {name is RuneName}
*/
export function is_rune(name) {
- return RUNES.includes(/** @type {RUNES[number]} */ (name));
+ return RUNES.includes(/** @type {RuneName} */ (name));
+}
+
+/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
+
+/**
+ * @param {string} name
+ * @returns {name is StateCreationRuneName}
+ */
+export function is_state_creation_rune(name) {
+ return STATE_CREATION_RUNES.includes(/** @type {StateCreationRuneName} */ (name));
}
/** List of elements that require raw contents and should not have SSR comments put in them */
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index a3a9979d65..9a198d097d 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.28.2';
+export const VERSION = '5.32.0';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
index e6551031cc..f5b116117e 100644
--- a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
@@ -4,7 +4,7 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
- '`$state(...)` can only be used as a variable declaration initializer or a class field',
+ '`$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.',
position: [33, 41]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js
new file mode 100644
index 0000000000..a24996677c
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'rune_invalid_name',
+ message: '`$state.foo` is not a valid rune'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js
new file mode 100644
index 0000000000..966f7bc63b
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js
@@ -0,0 +1,5 @@
+class State {
+ value = $state.foo();
+}
+
+export const state = new State();
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
index 1185ca6b45..592b2634a3 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
- message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+ 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.'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
index 476d7239a3..f31748d26a 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
@@ -4,6 +4,6 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
- '`$derived(...)` can only be used as a variable declaration initializer or a class field'
+ '`$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.'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
index 1185ca6b45..592b2634a3 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
- message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+ 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.'
}
});
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js
new file mode 100644
index 0000000000..56ba73b064
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+// https://github.com/sveltejs/svelte/issues/15819
+export default test({
+ expect_hydration_error: true
+});
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html
new file mode 100644
index 0000000000..5179fb04a5
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html
@@ -0,0 +1 @@
+start
cond
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html
new file mode 100644
index 0000000000..2a1c323288
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html
@@ -0,0 +1 @@
+ start
cond
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte
new file mode 100644
index 0000000000..bfb4f2cdb8
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte
@@ -0,0 +1,5 @@
+
+
+start
{#if cond}cond
{/if}
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js
new file mode 100644
index 0000000000..56ba73b064
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+// https://github.com/sveltejs/svelte/issues/15819
+export default test({
+ expect_hydration_error: true
+});
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html
new file mode 100644
index 0000000000..f6c03b87c1
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html
@@ -0,0 +1 @@
+start
pre123 mid
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html
new file mode 100644
index 0000000000..c84efbb00b
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html
@@ -0,0 +1 @@
+start
pre123 mid
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte
new file mode 100644
index 0000000000..2c9a94686e
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte
@@ -0,0 +1,9 @@
+
+
+start
+pre123
+{#if cond}
+mid
+{/if}
diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
index 9e4f086aed..26012e1115 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
@@ -1,7 +1,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
index 2b6838a1d6..328966b63b 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
@@ -1,6 +1,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
index 6e5ab10310..1e763577df 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
@@ -1,2 +1,2 @@
-
-
\ No newline at end of file
+
+
diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte
new file mode 100644
index 0000000000..4ee619728d
--- /dev/null
+++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte
@@ -0,0 +1,10 @@
+
+
+{#snippet generic(val: T)}
+ {val}
+{/snippet}
+
+{#snippet complex_generic">>(val: T)}
+ {val}
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json
new file mode 100644
index 0000000000..37fb499e7b
--- /dev/null
+++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json
@@ -0,0 +1,244 @@
+{
+ "html": {
+ "type": "Fragment",
+ "start": 30,
+ "end": 192,
+ "children": [
+ {
+ "type": "Text",
+ "start": 28,
+ "end": 30,
+ "raw": "\n\n",
+ "data": "\n\n"
+ },
+ {
+ "type": "SnippetBlock",
+ "start": 30,
+ "end": 92,
+ "expression": {
+ "type": "Identifier",
+ "start": 40,
+ "end": 47,
+ "name": "generic"
+ },
+ "parameters": [
+ {
+ "type": "Identifier",
+ "start": 66,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 36
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "name": "val",
+ "typeAnnotation": {
+ "type": "TSTypeAnnotation",
+ "start": 69,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 39
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "typeAnnotation": {
+ "type": "TSTypeReference",
+ "start": 71,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 41
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "typeName": {
+ "type": "Identifier",
+ "start": 71,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 41
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "name": "T"
+ }
+ }
+ }
+ }
+ ],
+ "children": [
+ {
+ "type": "MustacheTag",
+ "start": 76,
+ "end": 81,
+ "expression": {
+ "type": "Identifier",
+ "start": 77,
+ "end": 80,
+ "loc": {
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 5
+ }
+ },
+ "name": "val"
+ }
+ }
+ ],
+ "typeParams": "T extends string"
+ },
+ {
+ "type": "Text",
+ "start": 92,
+ "end": 94,
+ "raw": "\n\n",
+ "data": "\n\n"
+ },
+ {
+ "type": "SnippetBlock",
+ "start": 94,
+ "end": 192,
+ "expression": {
+ "type": "Identifier",
+ "start": 104,
+ "end": 119,
+ "name": "complex_generic"
+ },
+ "parameters": [
+ {
+ "type": "Identifier",
+ "start": 166,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 72
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "name": "val",
+ "typeAnnotation": {
+ "type": "TSTypeAnnotation",
+ "start": 169,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 75
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "typeAnnotation": {
+ "type": "TSTypeReference",
+ "start": 171,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 77
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "typeName": {
+ "type": "Identifier",
+ "start": 171,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 77
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "name": "T"
+ }
+ }
+ }
+ }
+ ],
+ "children": [
+ {
+ "type": "MustacheTag",
+ "start": 176,
+ "end": 181,
+ "expression": {
+ "type": "Identifier",
+ "start": 177,
+ "end": 180,
+ "loc": {
+ "start": {
+ "line": 9,
+ "column": 2
+ },
+ "end": {
+ "line": 9,
+ "column": 5
+ }
+ },
+ "name": "val"
+ }
+ }
+ ],
+ "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">"
+ }
+ ]
+ },
+ "instance": {
+ "type": "Script",
+ "start": 0,
+ "end": 28,
+ "context": "default",
+ "content": {
+ "type": "Program",
+ "start": 18,
+ "end": 19,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 0
+ }
+ },
+ "body": [],
+ "sourceType": "module"
+ }
+ }
+}
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte
new file mode 100644
index 0000000000..9faae8d1bf
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte
@@ -0,0 +1 @@
+ {}} {@attach (node) => {}}>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json
new file mode 100644
index 0000000000..42e9880fcc
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json
@@ -0,0 +1,141 @@
+{
+ "css": null,
+ "js": [],
+ "start": 0,
+ "end": 57,
+ "type": "Root",
+ "fragment": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "RegularElement",
+ "start": 0,
+ "end": 57,
+ "name": "div",
+ "attributes": [
+ {
+ "type": "AttachTag",
+ "start": 5,
+ "end": 27,
+ "expression": {
+ "type": "ArrowFunctionExpression",
+ "start": 14,
+ "end": 26,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 14
+ },
+ "end": {
+ "line": 1,
+ "column": 26
+ }
+ },
+ "id": null,
+ "expression": false,
+ "generator": false,
+ "async": false,
+ "params": [
+ {
+ "type": "Identifier",
+ "start": 15,
+ "end": 19,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 15
+ },
+ "end": {
+ "line": 1,
+ "column": 19
+ }
+ },
+ "name": "node"
+ }
+ ],
+ "body": {
+ "type": "BlockStatement",
+ "start": 24,
+ "end": 26,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 24
+ },
+ "end": {
+ "line": 1,
+ "column": 26
+ }
+ },
+ "body": []
+ }
+ }
+ },
+ {
+ "type": "AttachTag",
+ "start": 28,
+ "end": 50,
+ "expression": {
+ "type": "ArrowFunctionExpression",
+ "start": 37,
+ "end": 49,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 37
+ },
+ "end": {
+ "line": 1,
+ "column": 49
+ }
+ },
+ "id": null,
+ "expression": false,
+ "generator": false,
+ "async": false,
+ "params": [
+ {
+ "type": "Identifier",
+ "start": 38,
+ "end": 42,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 38
+ },
+ "end": {
+ "line": 1,
+ "column": 42
+ }
+ },
+ "name": "node"
+ }
+ ],
+ "body": {
+ "type": "BlockStatement",
+ "start": 47,
+ "end": 49,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 47
+ },
+ "end": {
+ "line": 1,
+ "column": 49
+ }
+ },
+ "body": []
+ }
+ }
+ }
+ ],
+ "fragment": {
+ "type": "Fragment",
+ "nodes": []
+ }
+ }
+ ]
+ },
+ "options": null
+}
diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte
new file mode 100644
index 0000000000..4ee619728d
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte
@@ -0,0 +1,10 @@
+
+
+{#snippet generic(val: T)}
+ {val}
+{/snippet}
+
+{#snippet complex_generic">>(val: T)}
+ {val}
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json
new file mode 100644
index 0000000000..b66ee7288f
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json
@@ -0,0 +1,299 @@
+{
+ "css": null,
+ "js": [],
+ "start": 30,
+ "end": 192,
+ "type": "Root",
+ "fragment": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "Text",
+ "start": 28,
+ "end": 30,
+ "raw": "\n\n",
+ "data": "\n\n"
+ },
+ {
+ "type": "SnippetBlock",
+ "start": 30,
+ "end": 92,
+ "expression": {
+ "type": "Identifier",
+ "start": 40,
+ "end": 47,
+ "name": "generic"
+ },
+ "typeParams": "T extends string",
+ "parameters": [
+ {
+ "type": "Identifier",
+ "start": 66,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 36
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "name": "val",
+ "typeAnnotation": {
+ "type": "TSTypeAnnotation",
+ "start": 69,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 39
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "typeAnnotation": {
+ "type": "TSTypeReference",
+ "start": 71,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 41
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "typeName": {
+ "type": "Identifier",
+ "start": 71,
+ "end": 72,
+ "loc": {
+ "start": {
+ "line": 4,
+ "column": 41
+ },
+ "end": {
+ "line": 4,
+ "column": 42
+ }
+ },
+ "name": "T"
+ }
+ }
+ }
+ }
+ ],
+ "body": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "Text",
+ "start": 74,
+ "end": 76,
+ "raw": "\n\t",
+ "data": "\n\t"
+ },
+ {
+ "type": "ExpressionTag",
+ "start": 76,
+ "end": 81,
+ "expression": {
+ "type": "Identifier",
+ "start": 77,
+ "end": 80,
+ "loc": {
+ "start": {
+ "line": 5,
+ "column": 2
+ },
+ "end": {
+ "line": 5,
+ "column": 5
+ }
+ },
+ "name": "val"
+ }
+ },
+ {
+ "type": "Text",
+ "start": 81,
+ "end": 82,
+ "raw": "\n",
+ "data": "\n"
+ }
+ ]
+ }
+ },
+ {
+ "type": "Text",
+ "start": 92,
+ "end": 94,
+ "raw": "\n\n",
+ "data": "\n\n"
+ },
+ {
+ "type": "SnippetBlock",
+ "start": 94,
+ "end": 192,
+ "expression": {
+ "type": "Identifier",
+ "start": 104,
+ "end": 119,
+ "name": "complex_generic"
+ },
+ "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">",
+ "parameters": [
+ {
+ "type": "Identifier",
+ "start": 166,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 72
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "name": "val",
+ "typeAnnotation": {
+ "type": "TSTypeAnnotation",
+ "start": 169,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 75
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "typeAnnotation": {
+ "type": "TSTypeReference",
+ "start": 171,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 77
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "typeName": {
+ "type": "Identifier",
+ "start": 171,
+ "end": 172,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 77
+ },
+ "end": {
+ "line": 8,
+ "column": 78
+ }
+ },
+ "name": "T"
+ }
+ }
+ }
+ }
+ ],
+ "body": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "Text",
+ "start": 174,
+ "end": 176,
+ "raw": "\n\t",
+ "data": "\n\t"
+ },
+ {
+ "type": "ExpressionTag",
+ "start": 176,
+ "end": 181,
+ "expression": {
+ "type": "Identifier",
+ "start": 177,
+ "end": 180,
+ "loc": {
+ "start": {
+ "line": 9,
+ "column": 2
+ },
+ "end": {
+ "line": 9,
+ "column": 5
+ }
+ },
+ "name": "val"
+ }
+ },
+ {
+ "type": "Text",
+ "start": 181,
+ "end": 182,
+ "raw": "\n",
+ "data": "\n"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "options": null,
+ "instance": {
+ "type": "Script",
+ "start": 0,
+ "end": 28,
+ "context": "default",
+ "content": {
+ "type": "Program",
+ "start": 18,
+ "end": 19,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 0
+ }
+ },
+ "body": [],
+ "sourceType": "module"
+ },
+ "attributes": [
+ {
+ "type": "Attribute",
+ "start": 8,
+ "end": 17,
+ "name": "lang",
+ "value": [
+ {
+ "start": 14,
+ "end": 16,
+ "type": "Text",
+ "raw": "ts",
+ "data": "ts"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js
new file mode 100644
index 0000000000..5d37252358
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, logs }) {
+ assert.deepEqual(logs, ['up']);
+
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ assert.deepEqual(logs, ['up']);
+
+ flushSync(() => button?.click());
+ assert.deepEqual(logs, ['up', 'down']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte
new file mode 100644
index 0000000000..fbec108c7a
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte
@@ -0,0 +1,15 @@
+
+
+ state.count++}>{state.count}
+
+{#if state.count < 2}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js
new file mode 100644
index 0000000000..1be4737069
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: `
`,
+ html: `DIV
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
new file mode 100644
index 0000000000..1a1f74e4a9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
@@ -0,0 +1 @@
+ node.textContent = node.nodeName}>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte
new file mode 100644
index 0000000000..6760da61fa
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js
new file mode 100644
index 0000000000..23907c62d2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js
@@ -0,0 +1,15 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `update
`,
+
+ test({ target, assert, logs }) {
+ const button = target.querySelector('button');
+
+ assert.deepEqual(logs, ['one DIV']);
+
+ flushSync(() => button?.click());
+ assert.deepEqual(logs, ['one DIV', 'cleanup one', 'two DIV']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte
new file mode 100644
index 0000000000..d1d7e65126
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte
@@ -0,0 +1,30 @@
+
+
+update
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte
new file mode 100644
index 0000000000..6760da61fa
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js
new file mode 100644
index 0000000000..b6ef016be5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js
@@ -0,0 +1,14 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: `update
`,
+ html: `update one
`,
+
+ test({ target, assert }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ assert.htmlEqual(target.innerHTML, 'update two
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
new file mode 100644
index 0000000000..29e26689db
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
@@ -0,0 +1,15 @@
+
+
+ message = 'two'}>update
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
new file mode 100644
index 0000000000..2e53f0d29d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
@@ -0,0 +1,116 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn, btn2, btn3] = target.querySelectorAll('button');
+
+ // both logs on creation it will not log on change
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']);
+
+ // clicking the first button logs the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]);
+
+ // clicking the second button logs the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]);
+
+ // updating the arguments logs the update function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment'
+ ]);
+
+ // clicking the first button again shows the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1
+ ]);
+
+ // clicking the second button again shows the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1
+ ]);
+
+ // unmounting logs the destroy function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1,
+ 'destroy',
+ 'action',
+ 'destroy',
+ 'attachment'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
new file mode 100644
index 0000000000..35079aa15e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
@@ -0,0 +1,37 @@
+
+
+{#if count < 2}
+
+ count)}>
+{/if}
+
+ count++}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js
new file mode 100644
index 0000000000..7d0502590b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js
@@ -0,0 +1,14 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: `
increment `,
+ html: `1
increment `,
+
+ test: ({ assert, target }) => {
+ const btn = target.querySelector('button');
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, `2
increment `);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
new file mode 100644
index 0000000000..9fa3cfdb67
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
@@ -0,0 +1,6 @@
+
+
+ node.textContent = value}>
+ value += 1}>increment
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js
new file mode 100644
index 0000000000..96fc207450
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js
@@ -0,0 +1,8 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, logs, target }) {
+ assert.deepEqual(logs, ['hello']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte
new file mode 100644
index 0000000000..dbd8c47ada
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js
new file mode 100644
index 0000000000..1be4737069
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: `
`,
+ html: `DIV
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte
new file mode 100644
index 0000000000..bd4b52342f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte
@@ -0,0 +1 @@
+ node.textContent = node.nodeName}>
diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js
new file mode 100644
index 0000000000..810ac338a5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ // check that this is a runtime error, not a compile time error
+ // caused by over-eager partial-evaluation
+ error: 'Cannot convert invalid to a BigInt'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte
new file mode 100644
index 0000000000..126528bad2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte
@@ -0,0 +1,5 @@
+
+
+{invalid}
diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js
new file mode 100644
index 0000000000..0fdeabfe0b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ btn?.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ inc
+ a:1
+ b:2
+ c:3
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte
new file mode 100644
index 0000000000..746f22b1e6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte
@@ -0,0 +1,28 @@
+
+
+ counter.inc()}>inc
+
+{#key 1}a:{counter.a}
{/key}
+{#key 2}b:{counter.b}
{/key}
+{#key 3}c:{counter.c}
{/key}
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js
rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte
rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js
new file mode 100644
index 0000000000..dd847ce2f2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `10 `,
+ ssrHtml: `0 `,
+
+ async test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `10 `);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
new file mode 100644
index 0000000000..3d8ea41418
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
@@ -0,0 +1,13 @@
+
+
+ counter.count++}>{counter.count}
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: `10 `,
+ ssrHtml: `0 `,
+
+ async test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `10 `);
+ }
+});
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 @@
+
+
+ counter.count++}>{counter.count}
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 @@
+
+
+
+
+
+ clicks: {someLogic.someValue}
+
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: `0 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `1 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `2 `);
+ }
+});
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 @@
+
+
+ counter.count++}>{counter.count}
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: `10: 20 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `11: 22 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `12: 24 `);
+ }
+});
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 @@
+
+
+{counter.count}: {counter.custom}
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: `20 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `22 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `24 `);
+ }
+});
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 @@
+
+
+{counter.doubled}
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/runtime-runes/samples/nullish-empty-string/_config.js b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js
new file mode 100644
index 0000000000..84e97e9735
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: '[]'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte
new file mode 100644
index 0000000000..efe39b91fe
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte
@@ -0,0 +1 @@
+[{undefined ?? null}]
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js
new file mode 100644
index 0000000000..95556f4737
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ async test({ assert, errors }) {
+ assert.deepEqual(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte
new file mode 100644
index 0000000000..d834551221
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte
@@ -0,0 +1,5 @@
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte
new file mode 100644
index 0000000000..a2e7d6d8a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte
@@ -0,0 +1,9 @@
+
+
+{Object.keys(props)}
+
+{#if $$slots.foo}
+ foo exists
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js
new file mode 100644
index 0000000000..3f6fb4143c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ html: `
+ a
+ foo exists
+ `
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte
new file mode 100644
index 0000000000..3535da7132
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte
@@ -0,0 +1,7 @@
+
+
+
+ foo
+
diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
index 47f297bce9..b2ef29ccaf 100644
--- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
@@ -7,10 +7,12 @@ let c = 3;
let d = 4;
export function update(array) {
- (
- $.set(a, array[0], true),
- $.set(b, array[1], true)
- );
+ {
+ let [$$1, $$2] = array;
+
+ $.set(a, $$1, true);
+ $.set(b, $$2, true);
+ };
[c, d] = array;
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
index 5bc9766acf..f661dbc01d 100644
--- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
@@ -8,7 +8,7 @@ export default function Purity($$anchor) {
var fragment = root();
var p = $.first_child(fragment);
- p.textContent = 0;
+ p.textContent = '0';
var p_1 = $.sibling(p, 2);
diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte
new file mode 100644
index 0000000000..21587e5f4f
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte
@@ -0,0 +1,8 @@
+
+defabc
+Hello, {name}!
+
+
+defabc
diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json
new file mode 100644
index 0000000000..6e70193c6c
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json
@@ -0,0 +1,50 @@
+[
+ {
+ "code": "bidirectional_control_characters",
+ "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
+ "start": {
+ "line": 2,
+ "column": 15
+ },
+ "end": {
+ "line": 2,
+ "column": 58
+ }
+ },
+ {
+ "code": "bidirectional_control_characters",
+ "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
+ "start": {
+ "line": 4,
+ "column": 0
+ },
+ "end": {
+ "line": 4,
+ "column": 2
+ }
+ },
+ {
+ "code": "bidirectional_control_characters",
+ "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
+ "start": {
+ "line": 4,
+ "column": 5
+ },
+ "end": {
+ "line": 4,
+ "column": 7
+ }
+ },
+ {
+ "code": "bidirectional_control_characters",
+ "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
+ "start": {
+ "line": 4,
+ "column": 10
+ },
+ "end": {
+ "line": 4,
+ "column": 12
+ }
+ }
+]
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/tests/validator/samples/implicitly-closed-by-parent/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte
new file mode 100644
index 0000000000..f67eba18b8
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte
@@ -0,0 +1,6 @@
+
+
+
+
+
hello
+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json
new file mode 100644
index 0000000000..1316a2b65b
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json
@@ -0,0 +1,26 @@
+[
+ {
+ "code": "element_implicitly_closed",
+ "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.",
+ "start": {
+ "line": 1,
+ "column": 6
+ },
+ "end": {
+ "line": 1,
+ "column": 25
+ }
+ },
+ {
+ "code": "element_implicitly_closed",
+ "message": "This element is implicitly closed by the following ` `, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.",
+ "start": {
+ "line": 4,
+ "column": 1
+ },
+ "end": {
+ "line": 4,
+ "column": 20
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte
new file mode 100644
index 0000000000..7721f2f380
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json
new file mode 100644
index 0000000000..6ea36c5a50
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json
@@ -0,0 +1,26 @@
+[
+ {
+ "code": "element_implicitly_closed",
+ "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.",
+ "start": {
+ "line": 2,
+ "column": 1
+ },
+ "end": {
+ "line": 2,
+ "column": 18
+ }
+ },
+ {
+ "code": "element_implicitly_closed",
+ "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.",
+ "start": {
+ "line": 8,
+ "column": 1
+ },
+ "end": {
+ "line": 8,
+ "column": 18
+ }
+ }
+]
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index bd936e9248..a86e2c5b26 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -653,6 +653,145 @@ declare module 'svelte/animate' {
export {};
}
+declare module 'svelte/attachments' {
+ /**
+ * An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
+ * to the DOM, and optionally returns a function that is called when the element is later removed.
+ *
+ * It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
+ * a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
+ */
+ export interface Attachment {
+ (element: T): void | (() => void);
+ }
+ /**
+ * Creates an object key that will be recognised as an attachment when the object is spread onto an element,
+ * as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
+ * is generally not needed when building an app.
+ *
+ * ```svelte
+ *
+ *
+ * click me
+ * ```
+ * @since 5.29
+ */
+ export function createAttachmentKey(): symbol;
+ /**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ * ...
+ *
+ *
+ * bar)}>...
+ * ```
+ * */
+ export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment;
+ /**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ * ...
+ *
+ *
+ * bar)}>...
+ * ```
+ * */
+ export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment;
+ /**
+ * Actions can return an object containing the two properties defined in this interface. Both are optional.
+ * - update: An action can have a parameter. This method will be called whenever that parameter changes,
+ * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both
+ * mean that the action accepts no parameters.
+ * - destroy: Method that is called after the element is unmounted
+ *
+ * Additionally, you can specify which additional attributes and events the action enables on the applied element.
+ * This applies to TypeScript typings only and has no effect at runtime.
+ *
+ * Example usage:
+ * ```ts
+ * interface Attributes {
+ * newprop?: string;
+ * 'on:event': (e: CustomEvent) => void;
+ * }
+ *
+ * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn {
+ * // ...
+ * return {
+ * update: (updatedParameter) => {...},
+ * destroy: () => {...}
+ * };
+ * }
+ * ```
+ */
+ interface ActionReturn<
+ Parameter = undefined,
+ Attributes extends Record = Record
+ > {
+ update?: (parameter: Parameter) => void;
+ destroy?: () => void;
+ /**
+ * ### DO NOT USE THIS
+ * This exists solely for type-checking and has no effect at runtime.
+ * Set this through the `Attributes` generic instead.
+ */
+ $$_attributes?: Attributes;
+ }
+
+ /**
+ * Actions are functions that are called when an element is created.
+ * You can use this interface to type such actions.
+ * The following example defines an action that only works on `` elements
+ * and optionally accepts a parameter which it has a default value for:
+ * ```ts
+ * export const myAction: Action
= (node, param = { someProperty: true }) => {
+ * // ...
+ * }
+ * ```
+ * `Action` and `Action` both signal that the action accepts no parameters.
+ *
+ * You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has.
+ * See interface `ActionReturn` for more details.
+ */
+ interface Action<
+ Element = HTMLElement,
+ Parameter = undefined,
+ Attributes extends Record = Record
+ > {
+ (
+ ...args: undefined extends Parameter
+ ? [node: Node, parameter?: Parameter]
+ : [node: Node, parameter: Parameter]
+ ): void | ActionReturn;
+ }
+
+ // Implementation notes:
+ // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode
+
+ export {};
+}
+
declare module 'svelte/compiler' {
import type { SourceMap } from 'magic-string';
import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
@@ -1089,6 +1228,12 @@ declare module 'svelte/compiler' {
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
}
+ /** A `{@attach foo(...)} tag */
+ export interface AttachTag extends BaseNode {
+ type: 'AttachTag';
+ expression: Expression;
+ }
+
/** An `animate:` directive */
export interface AnimateDirective extends BaseNode {
type: 'AnimateDirective';
@@ -1171,7 +1316,7 @@ declare module 'svelte/compiler' {
interface BaseElement extends BaseNode {
name: string;
- attributes: Array;
+ attributes: Array;
fragment: Fragment;
}
@@ -1291,6 +1436,7 @@ declare module 'svelte/compiler' {
type: 'SnippetBlock';
expression: Identifier;
parameters: Pattern[];
+ typeParams?: string;
body: Fragment;
}
@@ -1351,7 +1497,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
@@ -1361,6 +1513,7 @@ declare module 'svelte/compiler' {
| AST.Attribute
| AST.SpreadAttribute
| Directive
+ | AST.AttachTag
| AST.Comment
| Block;
diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json
index 5aee92ab17..3ab65ac4b5 100644
--- a/playgrounds/sandbox/package.json
+++ b/playgrounds/sandbox/package.json
@@ -18,7 +18,7 @@
"polka": "^1.0.0-next.25",
"svelte": "workspace:*",
"tinyglobby": "^0.2.12",
- "vite": "^5.4.18",
+ "vite": "^5.4.19",
"vite-plugin-inspect": "^0.8.4"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3518b0e57e..3d5db0fd8e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -152,7 +152,7 @@ importers:
devDependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^4.0.0-next.6
- version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
polka:
specifier: ^1.0.0-next.25
version: 1.0.0-next.25
@@ -163,11 +163,11 @@ importers:
specifier: ^0.2.12
version: 0.2.12
vite:
- specifier: ^5.4.18
- version: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ specifier: ^5.4.19
+ version: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
vite-plugin-inspect:
specifier: ^0.8.4
- version: 0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ version: 0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
packages:
@@ -408,8 +408,8 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
- '@eslint-community/eslint-utils@4.5.1':
- resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==}
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
@@ -551,8 +551,8 @@ packages:
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm-eabi@4.39.0':
- resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
+ '@rollup/rollup-android-arm-eabi@4.40.2':
+ resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
cpu: [arm]
os: [android]
@@ -561,8 +561,8 @@ packages:
cpu: [arm64]
os: [android]
- '@rollup/rollup-android-arm64@4.39.0':
- resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==}
+ '@rollup/rollup-android-arm64@4.40.2':
+ resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==}
cpu: [arm64]
os: [android]
@@ -571,8 +571,8 @@ packages:
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-arm64@4.39.0':
- resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==}
+ '@rollup/rollup-darwin-arm64@4.40.2':
+ resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==}
cpu: [arm64]
os: [darwin]
@@ -581,18 +581,18 @@ packages:
cpu: [x64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.39.0':
- resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==}
+ '@rollup/rollup-darwin-x64@4.40.2':
+ resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.39.0':
- resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==}
+ '@rollup/rollup-freebsd-arm64@4.40.2':
+ resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.39.0':
- resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==}
+ '@rollup/rollup-freebsd-x64@4.40.2':
+ resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==}
cpu: [x64]
os: [freebsd]
@@ -601,8 +601,8 @@ packages:
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
- resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.2':
+ resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
cpu: [arm]
os: [linux]
@@ -611,8 +611,8 @@ packages:
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-musleabihf@4.39.0':
- resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==}
+ '@rollup/rollup-linux-arm-musleabihf@4.40.2':
+ resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
cpu: [arm]
os: [linux]
@@ -621,8 +621,8 @@ packages:
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-gnu@4.39.0':
- resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==}
+ '@rollup/rollup-linux-arm64-gnu@4.40.2':
+ resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
cpu: [arm64]
os: [linux]
@@ -631,13 +631,13 @@ packages:
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-musl@4.39.0':
- resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==}
+ '@rollup/rollup-linux-arm64-musl@4.40.2':
+ resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
- resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==}
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
+ resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
cpu: [loong64]
os: [linux]
@@ -646,8 +646,8 @@ packages:
cpu: [ppc64]
os: [linux]
- '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
- resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==}
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
+ resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
cpu: [ppc64]
os: [linux]
@@ -656,13 +656,13 @@ packages:
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-riscv64-gnu@4.39.0':
- resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==}
+ '@rollup/rollup-linux-riscv64-gnu@4.40.2':
+ resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-riscv64-musl@4.39.0':
- resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==}
+ '@rollup/rollup-linux-riscv64-musl@4.40.2':
+ resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
cpu: [riscv64]
os: [linux]
@@ -671,8 +671,8 @@ packages:
cpu: [s390x]
os: [linux]
- '@rollup/rollup-linux-s390x-gnu@4.39.0':
- resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==}
+ '@rollup/rollup-linux-s390x-gnu@4.40.2':
+ resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
cpu: [s390x]
os: [linux]
@@ -681,8 +681,8 @@ packages:
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-gnu@4.39.0':
- resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==}
+ '@rollup/rollup-linux-x64-gnu@4.40.2':
+ resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
cpu: [x64]
os: [linux]
@@ -691,8 +691,8 @@ packages:
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-musl@4.39.0':
- resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==}
+ '@rollup/rollup-linux-x64-musl@4.40.2':
+ resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
cpu: [x64]
os: [linux]
@@ -701,8 +701,8 @@ packages:
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-arm64-msvc@4.39.0':
- resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==}
+ '@rollup/rollup-win32-arm64-msvc@4.40.2':
+ resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
cpu: [arm64]
os: [win32]
@@ -711,8 +711,8 @@ packages:
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.39.0':
- resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==}
+ '@rollup/rollup-win32-ia32-msvc@4.40.2':
+ resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==}
cpu: [ia32]
os: [win32]
@@ -721,8 +721,8 @@ packages:
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.39.0':
- resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==}
+ '@rollup/rollup-win32-x64-msvc@4.40.2':
+ resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==}
cpu: [x64]
os: [win32]
@@ -816,8 +816,8 @@ packages:
resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/scope-manager@8.29.1':
- resolution: {integrity: sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==}
+ '@typescript-eslint/scope-manager@8.32.1':
+ resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/type-utils@8.26.0':
@@ -831,8 +831,8 @@ packages:
resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/types@8.29.1':
- resolution: {integrity: sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==}
+ '@typescript-eslint/types@8.32.1':
+ resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.26.0':
@@ -841,8 +841,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/typescript-estree@8.29.1':
- resolution: {integrity: sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==}
+ '@typescript-eslint/typescript-estree@8.32.1':
+ resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
@@ -854,8 +854,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/utils@8.29.1':
- resolution: {integrity: sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==}
+ '@typescript-eslint/utils@8.32.1':
+ resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -865,8 +865,8 @@ packages:
resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/visitor-keys@8.29.1':
- resolution: {integrity: sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==}
+ '@typescript-eslint/visitor-keys@8.32.1':
+ resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@2.0.5':
@@ -1974,8 +1974,8 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
- rollup@4.39.0:
- resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
+ rollup@4.40.2:
+ resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -2013,6 +2013,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -2310,8 +2315,8 @@ packages:
terser:
optional: true
- vite@5.4.18:
- resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==}
+ vite@5.4.19:
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -2711,7 +2716,7 @@ snapshots:
eslint: 9.9.1
eslint-visitor-keys: 3.4.3
- '@eslint-community/eslint-utils@4.5.1(eslint@9.9.1)':
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.9.1)':
dependencies:
eslint: 9.9.1
eslint-visitor-keys: 3.4.3
@@ -2860,120 +2865,120 @@ snapshots:
optionalDependencies:
rollup: 4.22.4
- '@rollup/pluginutils@5.1.0(rollup@4.39.0)':
+ '@rollup/pluginutils@5.1.0(rollup@4.40.2)':
dependencies:
'@types/estree': 1.0.6
estree-walker: 2.0.2
picomatch: 2.3.1
optionalDependencies:
- rollup: 4.39.0
+ rollup: 4.40.2
'@rollup/rollup-android-arm-eabi@4.22.4':
optional: true
- '@rollup/rollup-android-arm-eabi@4.39.0':
+ '@rollup/rollup-android-arm-eabi@4.40.2':
optional: true
'@rollup/rollup-android-arm64@4.22.4':
optional: true
- '@rollup/rollup-android-arm64@4.39.0':
+ '@rollup/rollup-android-arm64@4.40.2':
optional: true
'@rollup/rollup-darwin-arm64@4.22.4':
optional: true
- '@rollup/rollup-darwin-arm64@4.39.0':
+ '@rollup/rollup-darwin-arm64@4.40.2':
optional: true
'@rollup/rollup-darwin-x64@4.22.4':
optional: true
- '@rollup/rollup-darwin-x64@4.39.0':
+ '@rollup/rollup-darwin-x64@4.40.2':
optional: true
- '@rollup/rollup-freebsd-arm64@4.39.0':
+ '@rollup/rollup-freebsd-arm64@4.40.2':
optional: true
- '@rollup/rollup-freebsd-x64@4.39.0':
+ '@rollup/rollup-freebsd-x64@4.40.2':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.22.4':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.2':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.22.4':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+ '@rollup/rollup-linux-arm-musleabihf@4.40.2':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.22.4':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.39.0':
+ '@rollup/rollup-linux-arm64-gnu@4.40.2':
optional: true
'@rollup/rollup-linux-arm64-musl@4.22.4':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.39.0':
+ '@rollup/rollup-linux-arm64-musl@4.40.2':
optional: true
- '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.22.4':
optional: true
- '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.22.4':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+ '@rollup/rollup-linux-riscv64-gnu@4.40.2':
optional: true
- '@rollup/rollup-linux-riscv64-musl@4.39.0':
+ '@rollup/rollup-linux-riscv64-musl@4.40.2':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.22.4':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.39.0':
+ '@rollup/rollup-linux-s390x-gnu@4.40.2':
optional: true
'@rollup/rollup-linux-x64-gnu@4.22.4':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.39.0':
+ '@rollup/rollup-linux-x64-gnu@4.40.2':
optional: true
'@rollup/rollup-linux-x64-musl@4.22.4':
optional: true
- '@rollup/rollup-linux-x64-musl@4.39.0':
+ '@rollup/rollup-linux-x64-musl@4.40.2':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.22.4':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.39.0':
+ '@rollup/rollup-win32-arm64-msvc@4.40.2':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.22.4':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.39.0':
+ '@rollup/rollup-win32-ia32-msvc@4.40.2':
optional: true
'@rollup/rollup-win32-x64-msvc@4.22.4':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.39.0':
+ '@rollup/rollup-win32-x64-msvc@4.40.2':
optional: true
'@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)':
@@ -3000,25 +3005,25 @@ snapshots:
typescript: 5.5.4
typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4)
- '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
+ '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
dependencies:
- '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
debug: 4.4.0
svelte: link:packages/svelte
- vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- supports-color
- '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
+ '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
dependencies:
- '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
debug: 4.4.0
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.17
svelte: link:packages/svelte
- vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
- vitefu: 0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vitefu: 0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
transitivePeerDependencies:
- supports-color
@@ -3088,10 +3093,10 @@ snapshots:
'@typescript-eslint/types': 8.26.0
'@typescript-eslint/visitor-keys': 8.26.0
- '@typescript-eslint/scope-manager@8.29.1':
+ '@typescript-eslint/scope-manager@8.32.1':
dependencies:
- '@typescript-eslint/types': 8.29.1
- '@typescript-eslint/visitor-keys': 8.29.1
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/visitor-keys': 8.32.1
'@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)':
dependencies:
@@ -3106,7 +3111,7 @@ snapshots:
'@typescript-eslint/types@8.26.0': {}
- '@typescript-eslint/types@8.29.1': {}
+ '@typescript-eslint/types@8.32.1': {}
'@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)':
dependencies:
@@ -3122,15 +3127,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@8.29.1(typescript@5.5.4)':
+ '@typescript-eslint/typescript-estree@8.32.1(typescript@5.5.4)':
dependencies:
- '@typescript-eslint/types': 8.29.1
- '@typescript-eslint/visitor-keys': 8.29.1
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/visitor-keys': 8.32.1
debug: 4.4.0
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
- semver: 7.7.1
+ semver: 7.7.2
ts-api-utils: 2.1.0(typescript@5.5.4)
typescript: 5.5.4
transitivePeerDependencies:
@@ -3147,12 +3152,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.29.1(eslint@9.9.1)(typescript@5.5.4)':
+ '@typescript-eslint/utils@8.32.1(eslint@9.9.1)(typescript@5.5.4)':
dependencies:
- '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1)
- '@typescript-eslint/scope-manager': 8.29.1
- '@typescript-eslint/types': 8.29.1
- '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.5.4)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
+ '@typescript-eslint/scope-manager': 8.32.1
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.5.4)
eslint: 9.9.1
typescript: 5.5.4
transitivePeerDependencies:
@@ -3163,9 +3168,9 @@ snapshots:
'@typescript-eslint/types': 8.26.0
eslint-visitor-keys: 4.2.0
- '@typescript-eslint/visitor-keys@8.29.1':
+ '@typescript-eslint/visitor-keys@8.32.1':
dependencies:
- '@typescript-eslint/types': 8.29.1
+ '@typescript-eslint/types': 8.32.1
eslint-visitor-keys: 4.2.0
'@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
@@ -3491,7 +3496,7 @@ snapshots:
eslint-compat-utils@0.5.1(eslint@9.9.1):
dependencies:
eslint: 9.9.1
- semver: 7.7.1
+ semver: 7.7.2
eslint-config-prettier@9.1.0(eslint@9.9.1):
dependencies:
@@ -3499,7 +3504,7 @@ snapshots:
eslint-plugin-es-x@7.8.0(eslint@9.9.1):
dependencies:
- '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
'@eslint-community/regexpp': 4.12.1
eslint: 9.9.1
eslint-compat-utils: 0.5.1(eslint@9.9.1)
@@ -3508,8 +3513,8 @@ snapshots:
eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4):
dependencies:
- '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1)
- '@typescript-eslint/utils': 8.29.1(eslint@9.9.1)(typescript@5.5.4)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
+ '@typescript-eslint/utils': 8.32.1(eslint@9.9.1)(typescript@5.5.4)
enhanced-resolve: 5.18.1
eslint: 9.9.1
eslint-plugin-es-x: 7.8.0(eslint@9.9.1)
@@ -3517,7 +3522,7 @@ snapshots:
globals: 15.15.0
ignore: 5.3.2
minimatch: 9.0.5
- semver: 7.7.1
+ semver: 7.7.2
ts-declaration-location: 1.0.7(typescript@5.5.4)
transitivePeerDependencies:
- supports-color
@@ -3525,7 +3530,7 @@ snapshots:
eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte):
dependencies:
- '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
'@jridgewell/sourcemap-codec': 1.5.0
debug: 4.4.0
eslint: 9.9.1
@@ -3536,7 +3541,7 @@ snapshots:
postcss-load-config: 3.1.4(postcss@8.5.3)
postcss-safe-parser: 6.0.0(postcss@8.5.3)
postcss-selector-parser: 6.1.2
- semver: 7.7.1
+ semver: 7.7.2
svelte-eslint-parser: 0.43.0(svelte@packages+svelte)
optionalDependencies:
svelte: link:packages/svelte
@@ -4292,30 +4297,30 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.22.4
fsevents: 2.3.3
- rollup@4.39.0:
+ rollup@4.40.2:
dependencies:
'@types/estree': 1.0.7
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.39.0
- '@rollup/rollup-android-arm64': 4.39.0
- '@rollup/rollup-darwin-arm64': 4.39.0
- '@rollup/rollup-darwin-x64': 4.39.0
- '@rollup/rollup-freebsd-arm64': 4.39.0
- '@rollup/rollup-freebsd-x64': 4.39.0
- '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
- '@rollup/rollup-linux-arm-musleabihf': 4.39.0
- '@rollup/rollup-linux-arm64-gnu': 4.39.0
- '@rollup/rollup-linux-arm64-musl': 4.39.0
- '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
- '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
- '@rollup/rollup-linux-riscv64-gnu': 4.39.0
- '@rollup/rollup-linux-riscv64-musl': 4.39.0
- '@rollup/rollup-linux-s390x-gnu': 4.39.0
- '@rollup/rollup-linux-x64-gnu': 4.39.0
- '@rollup/rollup-linux-x64-musl': 4.39.0
- '@rollup/rollup-win32-arm64-msvc': 4.39.0
- '@rollup/rollup-win32-ia32-msvc': 4.39.0
- '@rollup/rollup-win32-x64-msvc': 4.39.0
+ '@rollup/rollup-android-arm-eabi': 4.40.2
+ '@rollup/rollup-android-arm64': 4.40.2
+ '@rollup/rollup-darwin-arm64': 4.40.2
+ '@rollup/rollup-darwin-x64': 4.40.2
+ '@rollup/rollup-freebsd-arm64': 4.40.2
+ '@rollup/rollup-freebsd-x64': 4.40.2
+ '@rollup/rollup-linux-arm-gnueabihf': 4.40.2
+ '@rollup/rollup-linux-arm-musleabihf': 4.40.2
+ '@rollup/rollup-linux-arm64-gnu': 4.40.2
+ '@rollup/rollup-linux-arm64-musl': 4.40.2
+ '@rollup/rollup-linux-loongarch64-gnu': 4.40.2
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2
+ '@rollup/rollup-linux-riscv64-gnu': 4.40.2
+ '@rollup/rollup-linux-riscv64-musl': 4.40.2
+ '@rollup/rollup-linux-s390x-gnu': 4.40.2
+ '@rollup/rollup-linux-x64-gnu': 4.40.2
+ '@rollup/rollup-linux-x64-musl': 4.40.2
+ '@rollup/rollup-win32-arm64-msvc': 4.40.2
+ '@rollup/rollup-win32-ia32-msvc': 4.40.2
+ '@rollup/rollup-win32-x64-msvc': 4.40.2
fsevents: 2.3.3
rrweb-cssom@0.7.1: {}
@@ -4347,6 +4352,8 @@ snapshots:
semver@7.7.1: {}
+ semver@7.7.2: {}
+
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@@ -4562,7 +4569,7 @@ snapshots:
debug: 4.4.0
es-module-lexer: 1.6.0
pathe: 1.1.2
- vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- '@types/node'
- less
@@ -4574,10 +4581,10 @@ snapshots:
- supports-color
- terser
- vite-plugin-inspect@0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
+ vite-plugin-inspect@0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
dependencies:
'@antfu/utils': 0.7.8
- '@rollup/pluginutils': 5.1.0(rollup@4.39.0)
+ '@rollup/pluginutils': 5.1.0(rollup@4.40.2)
debug: 4.4.0
error-stack-parser-es: 0.1.1
fs-extra: 11.2.0
@@ -4585,7 +4592,7 @@ snapshots:
perfect-debounce: 1.0.0
picocolors: 1.1.1
sirv: 2.0.4
- vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- rollup
- supports-color
@@ -4602,11 +4609,11 @@ snapshots:
sass: 1.70.0
terser: 5.27.0
- vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
+ vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.3
- rollup: 4.39.0
+ rollup: 4.40.2
optionalDependencies:
'@types/node': 20.12.7
fsevents: 2.3.3
@@ -4614,9 +4621,9 @@ snapshots:
sass: 1.70.0
terser: 5.27.0
- vitefu@0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
+ vitefu@0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
optionalDependencies:
- vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
dependencies: