diff --git a/.changeset/nervous-kids-shake.md b/.changeset/nervous-kids-shake.md
new file mode 100644
index 0000000000..3fc6429797
--- /dev/null
+++ b/.changeset/nervous-kids-shake.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: set deriveds as `CLEAN` if they are assigned to
diff --git a/.changeset/stupid-vans-draw.md b/.changeset/stupid-vans-draw.md
new file mode 100644
index 0000000000..24892f1e8f
--- /dev/null
+++ b/.changeset/stupid-vans-draw.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: better scope `:global()` with nesting selector `&`
diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml
index b1fba0b04b..3f1fca5a0b 100644
--- a/.github/workflows/pkg.pr.new-comment.yml
+++ b/.github/workflows/pkg.pr.new-comment.yml
@@ -6,6 +6,9 @@ on:
types:
- completed
+permissions:
+ pull-requests: write
+
jobs:
build:
name: 'Update comment'
diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js
index a5fc6d10a9..9d8d279c35 100644
--- a/benchmarking/compare/index.js
+++ b/benchmarking/compare/index.js
@@ -2,7 +2,6 @@ import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
-import { benchmarks } from '../benchmarks.js';
// if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean');
diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js
index 6fa58e2bac..a2e8646379 100644
--- a/benchmarking/compare/runner.js
+++ b/benchmarking/compare/runner.js
@@ -1,7 +1,7 @@
-import { benchmarks } from '../benchmarks.js';
+import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
const results = [];
-for (const benchmark of benchmarks) {
+for (const benchmark of reactivity_benchmarks) {
const result = await benchmark();
console.error(result.benchmark);
results.push(result);
diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md
index e035e6d6df..c7351729ff 100644
--- a/documentation/docs/01-introduction/02-getting-started.md
+++ b/documentation/docs/01-introduction/02-getting-started.md
@@ -2,7 +2,7 @@
title: Getting started
---
-We recommend using [SvelteKit](../kit), the official application framework from the Svelte team powered by [Vite](https://vite.dev/):
+We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with:
```bash
npx sv create myapp
@@ -15,7 +15,9 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
-You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
+You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
+
+>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md
index 24ab643b68..2464aa9295 100644
--- a/documentation/docs/02-runes/03-$derived.md
+++ b/documentation/docs/02-runes/03-$derived.md
@@ -52,6 +52,48 @@ Anything read synchronously inside the `$derived` expression (or `$derived.by` f
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
+## Overriding derived values
+
+Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user:
+
+```svelte
+
+
+
+```
+
+> [!NOTE] Prior to Svelte 5.25, deriveds were read-only.
+
+## Deriveds and reactivity
+
+Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
+
+```svelte
+let items = $state([...]);
+
+let index = $state(0);
+let selected = $derived(items[index]);
+```
+
+...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect.
+
## Update propagation
Svelte uses something called _push-pull reactivity_ โ when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull').
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index e346bceba8..ae1a2146c9 100644
--- a/documentation/docs/02-runes/04-$effect.md
+++ b/documentation/docs/02-runes/04-$effect.md
@@ -74,6 +74,8 @@ Teardown functions also run when the effect is destroyed, which happens when its
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run.
+If `$state` and `$derived` are used directly inside the `$effect` (for example, during creation of a [reactive class](https://svelte.dev/docs/svelte/$state#Classes)), those values will _not_ be treated as dependencies.
+
Values that are read _asynchronously_ โ after an `await` or inside a `setTimeout`, for example โ will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):
```ts
@@ -252,6 +254,8 @@ In general, `$effect` is best considered something of an escape hatch โ useful
> [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`.
+If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
+
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
```svelte
@@ -280,7 +284,7 @@ You might be tempted to do something convoluted with effects to link one value t
```
-Instead, use callbacks where possible ([demo](/playground/untitled#H4sIAAAAAAAACo1SMW6EMBD8imWluFMSIEUaDiKlvy5lSOHjlhOSMRZeTiDkv8deMEEJRcqdmZ1ZjzzxqpZgePo5cRw18JQA_sSVaPz0rnVk7iDRYxdhYA8vW4Wg0NnwzJRdrfGtUAVKQIYtCsly9pIkp4AZ7cQOezAoEA7JcWUkVBuCdol0dNWrEutWsV5fHfnhPQ5wZJMnCwyejxCh6G6A0V3IHk4zu_jOxzzPBxBld83PTr7xXrb3rUNw8PbiYJ3FP22oTIoLSComq5XuXTeu8LzgnVA3KDgj13wiQ8taRaJ82rzXskYM-URRlsXktejjgNLoo9e4fyf70_8EnwncySX1GuunX6kGRwnzR_BgaPNaGy3FmLJKwrCUeBM6ZUn0Cs2mOlp3vwthQJ5i14P9st9vZqQlsQIAAA==)):
+Instead, use `oninput` callbacks or โ better still โ [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)):
```svelte
```
-If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/playground/untitled#H4sIAAAAAAAACpWRwW6DMBBEf8WyekikFOihFwcq9TvqHkyyQUjGsfCCQMj_XnvBNKpy6Qn2DTOD1wu_tRocF18Lx9kCFwT4iRvVxenT2syNoDGyWjl4xi93g2AwxPDSXfrW4oc0EjUgwzsqzSr2VhTnxJwNHwf24lAhHIpjVDZNwy1KS5wlNoGMSg9wOCYksQccerMlv65p51X0p_Xpdt_4YEy9yTkmV3z4MJT579-bUqsaNB2kbI0dwlnCgirJe2UakJzVrbkKaqkWivasU1O1ULxnOVk3JU-Uxti0p_-vKO4no_enbQ_yXhnZn0aHs4b1jiJMK7q2zmo1C3bTMG3LaZQVrMjeoSPgaUtkDxePMCEX2Ie6b_8D4WyJJEwCAAA=)):
-
-```svelte
-
-
-
-
-
-```
-
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).
diff --git a/documentation/docs/02-runes/08-$host.md b/documentation/docs/02-runes/08-$host.md
index 7b5e041e5e..ba6f0a5b5b 100644
--- a/documentation/docs/02-runes/08-$host.md
+++ b/documentation/docs/02-runes/08-$host.md
@@ -2,7 +2,7 @@
title: $host
---
-When compiling a component as a custom element, the `$host` rune provides access to the host element, allowing you to (for example) dispatch custom events ([demo](/playground/untitled#H4sIAAAAAAAAE41Ry2rDMBD8FSECtqkTt1fHFpSSL-ix7sFRNkTEXglrnTYY_3uRlDgxTaEHIfYxs7szA9-rBizPPwZOZwM89wmecqxbF70as7InaMjltrWFR3mpkQDJ8pwXVnbKkKiwItUa3RGLVtk7gTHQXRDR2lXda4CY1D0SK9nCUk0QPyfrCovsRoNFe17aQOAwGncgO2gBqRzihJXiQrEs2csYOhQ-7HgKHaLIbpRhhBG-I2eD_8ciM4KnnOCbeE5dD2P6h0Dz0-Yi_arNhPLJXBtSGi2TvSXdbpqwdsXvjuYsC1veabvvUTog2ylrapKH2G2XsMFLS4uDthQnq2t1cwKkGOGLvYU5PvaQxLsxOkPmsm97Io1Mo2yUPF6VnOZFkw1RMoopKLKAE_9gmGxyDFMwMcwN-Bx_ABXQWmOtAgAA)):
+When compiling a component as a [custom element](custom-elements), the `$host` rune provides access to the host element, allowing you to (for example) dispatch custom events ([demo](/playground/untitled#H4sIAAAAAAAAE41Ry2rDMBD8FSECtqkTt1fHFpSSL-ix7sFRNkTEXglrnTYY_3uRlDgxTaEHIfYxs7szA9-rBizPPwZOZwM89wmecqxbF70as7InaMjltrWFR3mpkQDJ8pwXVnbKkKiwItUa3RGLVtk7gTHQXRDR2lXda4CY1D0SK9nCUk0QPyfrCovsRoNFe17aQOAwGncgO2gBqRzihJXiQrEs2csYOhQ-7HgKHaLIbpRhhBG-I2eD_8ciM4KnnOCbeE5dD2P6h0Dz0-Yi_arNhPLJXBtSGi2TvSXdbpqwdsXvjuYsC1veabvvUTog2ylrapKH2G2XsMFLS4uDthQnq2t1cwKkGOGLvYU5PvaQxLsxOkPmsm97Io1Mo2yUPF6VnOZFkw1RMoopKLKAE_9gmGxyDFMwMcwN-Bx_ABXQWmOtAgAA)):
```svelte
diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md
index 30799215b6..4204bcfe6d 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -2,129 +2,137 @@
title: Context
---
-
+Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`...
-Most state is component-level state that lives as long as its component lives. There's also section-wide or app-wide state however, which also needs to be handled somehow.
-
-The easiest way to do that is to create global state and just import that.
+```svelte
+
+
```
+...and the child retrieves it with `getContext`:
+
```svelte
-
+
+
+
{message}, inside Child.svelte
```
-This has a few drawbacks though:
+This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)):
+
+```svelte
+
+
+
+```
-- it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs
-- it may give the false impression that certain state is global when in reality it should only used in a certain part of your app
+The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value.
-To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems.
+In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions.
-## Setting and getting context
+## Using context with state
-To associate an arbitrary object with the current component, use `setContext`.
+You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))...
```svelte
+
+
+
+
+
+
```
-The context is then available to children of the component (including slotted content) with `getContext`.
+...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' โ in other words instead of this...
```svelte
-
+
```
-`setContext` and `getContext` solve the above problems:
+...you must do this:
-- the state is not global, it's scoped to the component. That way it's safe to render your components on the server and not leak state
-- it's clear that the state is not global but rather scoped to a specific component tree and therefore can't be used in other parts of your app
+```svelte
+
+```
-> [!NOTE] `setContext`/`getContext` must be called during component initialisation.
+Svelte will warn you if you get it wrong.
-Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive.
+## Type-safe context
-```svelte
-
-
+```js
+/// file: context.js
+// @filename: ambient.d.ts
+interface User {}
-
-```
+// @filename: index.js
+// ---cut---
+import { getContext, setContext } from 'svelte';
-```svelte
-
-
+/** @param {User} user */
+export function setUserContext(user) {
+ setContext(key, user);
+}
-
Count is {value.count}
+export function getUserContext() {
+ return /** @type {User} */ (getContext(key));
+}
```
-To check whether a given `key` has been set in the context of a parent component, use `hasContext`.
+## Replacing global state
-```svelte
-
+ // ...
+});
```
-You can also retrieve the whole context map that belongs to the closest parent component using `getAllContexts`. This is useful, for example, if you programmatically create a component and want to pass the existing context to it.
+In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
+
```
-## Encapsulating context interactions
-
-The above methods are very unopinionated about how to use them. When your app grows in scale, it's worthwhile to encapsulate setting and getting the context into functions and properly type them.
-
-```ts
-// @errors: 2304
-import { getContext, setContext } from 'svelte';
-
-let userKey = Symbol('user');
-
-export function setUserContext(user: User) {
- setContext(userKey, user);
-}
-
-export function getUserContext(): User {
- return getContext(userKey) as User;
-}
-```
+...then the data may be accessible by the _next_ user. Context solves this problem because it is not shared between requests.
diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md
index 2b97ca796f..f051c46d73 100644
--- a/documentation/docs/06-runtime/03-lifecycle-hooks.md
+++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md
@@ -147,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru
}
function toggle() {
- toggleValue = !toggleValue;
+ theme = theme === 'dark' ? 'light' : 'dark';
}
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index 87ff40cf47..e502b7921a 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -10,13 +10,13 @@ You don't have to migrate to the new syntax right away - Svelte 5 still supports
At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign.
-### let -> $state
+### let โ $state
In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`:
```svelte
```
@@ -25,14 +25,14 @@ Nothing else changes. `count` is still the number itself, and you read and write
> [!DETAILS] Why we did this
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
-### $: -> $derived/$effect
+### $: โ $derived/$effect
In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune:
```svelte
```
@@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
```svelte
```
@@ -105,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
```svelte
@@ -192,9 +193,9 @@ This function is deprecated in Svelte 5. Instead, components should accept _call
```svelte
@@ -466,11 +467,11 @@ By now you should have a pretty good understanding of the before/after and how t
We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things:
- bump core dependencies in your `package.json`
-- migrate to runes (`let` -> `$state` etc)
-- migrate to event attributes for DOM elements (`on:click` -> `onclick`)
-- migrate slot creations to render tags (`` -> `{@render children()}`)
-- migrate slot usages to snippets (`
...
` -> `{#snippet x()}
...
{/snippet}`)
-- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`)
+- migrate to runes (`let` โ `$state` etc)
+- migrate to event attributes for DOM elements (`on:click` โ `onclick`)
+- migrate slot creations to render tags (`` โ `{@render children()}`)
+- migrate slot usages to snippets (`
...
` โ `{#snippet x()}
...
{/snippet}`)
+- migrate obvious component creations (`new Component(...)` โ `mount(Component, ...)`)
You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button.
diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md
index b56c27af86..ed5c6277c0 100644
--- a/documentation/docs/07-misc/99-faq.md
+++ b/documentation/docs/07-misc/99-faq.md
@@ -46,7 +46,7 @@ It will show up on hover.
- You can use markdown here.
- You can also use code blocks here.
- Usage:
- ```tsx
+ ```svelte
```
-->
@@ -96,7 +96,7 @@ However, you can use any router library. A lot of people use [page.js](https://g
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
-If you need hash-based routing on the client side, check out [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
+If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 2c2e0707ea..fd9419176d 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -122,14 +122,39 @@ Property descriptors defined on `$state` objects must contain `value` and always
Cannot set prototype of `$state` object
```
-### state_unsafe_local_read
+### state_unsafe_mutation
```
-Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
+Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
```
-### state_unsafe_mutation
+This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
+
+```svelte
+
+
+
+
{count} is even: {even}
+
{count} is odd: {odd}
```
-Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+
+This is forbidden because it introduces instability: if `
{count} is even: {even}
` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
+
+```js
+let count = 0;
+// ---cut---
+let even = $derived(count % 2 === 0);
+let odd = $derived(!even);
```
+
+If side-effects are unavoidable, use [`$effect`]($effect) instead.
diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md
index 284e9a7c3e..77d1df4cdd 100644
--- a/documentation/docs/98-reference/.generated/client-warnings.md
+++ b/documentation/docs/98-reference/.generated/client-warnings.md
@@ -161,7 +161,7 @@ Tried to unmount a component that was not mounted
### ownership_invalid_binding
```
-%parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+%parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
```
Consider three components `GrandParent`, `Parent` and `Child`. If you do ``, inside `GrandParent` pass on the variable via `` (note the missing `bind:`) and then do `` inside `Parent`, this warning is thrown.
@@ -171,11 +171,7 @@ To fix it, `bind:` to the value instead of just passing a property (i.e. in this
### ownership_invalid_mutation
```
-Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
-```
-
-```
-%component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
```
Consider the following code:
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index ea116014e7..a8c39aaf97 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -660,6 +660,12 @@ Cannot access a computed property of a rune
`%name%` is not a valid rune
```
+### rune_invalid_spread
+
+```
+`%rune%` cannot be called with a spread argument
+```
+
### rune_invalid_usage
```
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index a05938dacc..eb76e9a9e9 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,129 @@
# svelte
+## 5.25.9
+
+### Patch Changes
+
+- fix: allow `$.state` and `$.derived` to be treeshaken ([#15702](https://github.com/sveltejs/svelte/pull/15702))
+
+- fix: rework binding ownership validation ([#15678](https://github.com/sveltejs/svelte/pull/15678))
+
+## 5.25.8
+
+### Patch Changes
+
+- fix: address untracked_writes memory leak ([#15694](https://github.com/sveltejs/svelte/pull/15694))
+
+## 5.25.7
+
+### Patch Changes
+
+- fix: ensure clearing of old values happens independent of root flushes ([#15664](https://github.com/sveltejs/svelte/pull/15664))
+
+## 5.25.6
+
+### Patch Changes
+
+- fix: ignore generic type arguments while creating AST ([#15659](https://github.com/sveltejs/svelte/pull/15659))
+
+- fix: better consider component and its snippets during css pruning ([#15630](https://github.com/sveltejs/svelte/pull/15630))
+
+## 5.25.5
+
+### Patch Changes
+
+- fix: add setters to `$derived` class properties ([#15628](https://github.com/sveltejs/svelte/pull/15628))
+
+- fix: silence assignment warning on more function bindings ([#15644](https://github.com/sveltejs/svelte/pull/15644))
+
+- fix: make sure CSS is preserved during SSR with bindings ([#15645](https://github.com/sveltejs/svelte/pull/15645))
+
+## 5.25.4
+
+### Patch Changes
+
+- fix: support TS type assertions ([#15642](https://github.com/sveltejs/svelte/pull/15642))
+
+- fix: ensure `undefined` class still applies scoping class, if necessary ([#15643](https://github.com/sveltejs/svelte/pull/15643))
+
+## 5.25.3
+
+### Patch Changes
+
+- fix: prevent state runes from being called with spread ([#15585](https://github.com/sveltejs/svelte/pull/15585))
+
+## 5.25.2
+
+### Patch Changes
+
+- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581))
+
+## 5.25.1
+
+### Patch Changes
+
+- fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow ([#15577](https://github.com/sveltejs/svelte/pull/15577))
+
+## 5.25.0
+
+### Minor Changes
+
+- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570))
+
+## 5.24.1
+
+### Patch Changes
+
+- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300))
+
+- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574))
+
+## 5.24.0
+
+### Minor Changes
+
+- feat: allow state created in deriveds/effects to be written/read locally without self-invalidation ([#15553](https://github.com/sveltejs/svelte/pull/15553))
+
+### Patch Changes
+
+- fix: check if DOM prototypes are extensible ([#15569](https://github.com/sveltejs/svelte/pull/15569))
+
+- Keep inlined trailing JSDoc comments of properties when running svelte-migrate ([#15567](https://github.com/sveltejs/svelte/pull/15567))
+
+- fix: simplify set calls for proxyable values ([#15548](https://github.com/sveltejs/svelte/pull/15548))
+
+- fix: don't depend on deriveds created inside the current reaction ([#15564](https://github.com/sveltejs/svelte/pull/15564))
+
+## 5.23.2
+
+### Patch Changes
+
+- fix: don't hoist listeners that access non hoistable snippets ([#15534](https://github.com/sveltejs/svelte/pull/15534))
+
+## 5.23.1
+
+### Patch Changes
+
+- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506))
+
+- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277))
+
+- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511))
+
+- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313))
+
+- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473))
+
+- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492))
+
+- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516))
+
+## 5.23.0
+
+### Minor Changes
+
+- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469))
+
## 5.22.6
### Patch Changes
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 08687cafaf..99d87b4c09 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -1076,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
checked?: boolean | undefined | null;
dirname?: string | undefined | null;
disabled?: boolean | undefined | null;
+ files?: FileList | undefined | null;
form?: string | undefined | null;
formaction?: string | undefined | null;
formenctype?:
@@ -1087,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null;
formnovalidate?: boolean | undefined | null;
formtarget?: string | undefined | null;
+ group?: any | undefined | null;
height?: number | string | undefined | null;
indeterminate?: boolean | undefined | null;
list?: string | undefined | null;
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index ce1f222c63..ca06122cb5 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -80,10 +80,37 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Cannot set prototype of `$state` object
-## state_unsafe_local_read
-
-> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
-
## state_unsafe_mutation
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+
+This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
+
+```svelte
+
+
+
+
+
{count} is even: {even}
+
{count} is odd: {odd}
+```
+
+This is forbidden because it introduces instability: if `
{count} is even: {even}
` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
+
+```js
+let count = 0;
+// ---cut---
+let even = $derived(count % 2 === 0);
+let odd = $derived(!even);
+```
+
+If side-effects are unavoidable, use [`$effect`]($effect) instead.
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index 943cf6f01f..f8e9ebd8a0 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -132,7 +132,7 @@ During development, this error is often preceeded by a `console.error` detailing
## ownership_invalid_binding
-> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+> %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
Consider three components `GrandParent`, `Parent` and `Child`. If you do ``, inside `GrandParent` pass on the variable via `` (note the missing `bind:`) and then do `` inside `Parent`, this warning is thrown.
@@ -140,9 +140,7 @@ To fix it, `bind:` to the value instead of just passing a property (i.e. in this
## ownership_invalid_mutation
-> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
-
-> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+> Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
Consider the following code:
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 795c0b007d..aabcbeae48 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -162,6 +162,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `%name%` is not a valid rune
+## rune_invalid_spread
+
+> `%rune%` cannot be called with a spread argument
+
## rune_invalid_usage
> Cannot use `%rune%` rune in non-runes mode
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 53f03e3543..3fb843b3a2 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,18 +2,19 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.22.6",
+ "version": "5.25.9",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
"node": ">=18"
},
"files": [
+ "*.d.ts",
"src",
"!src/**/*.test.*",
+ "!src/**/*.d.ts",
"types",
"compiler",
- "*.d.ts",
"README.md"
],
"module": "src/index-client.js",
@@ -155,7 +156,7 @@
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
- "esrap": "^1.4.3",
+ "esrap": "^1.4.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 677b99fcff..6bf973948b 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -383,6 +383,16 @@ export function rune_invalid_name(node, name) {
e(node, 'rune_invalid_name', `\`${name}\` is not a valid rune\nhttps://svelte.dev/e/rune_invalid_name`);
}
+/**
+ * `%rune%` cannot be called with a spread argument
+ * @param {null | number | NodeLike} node
+ * @param {string} rune
+ * @returns {never}
+ */
+export function rune_invalid_spread(node, rune) {
+ e(node, 'rune_invalid_spread', `\`${rune}\` cannot be called with a spread argument\nhttps://svelte.dev/e/rune_invalid_spread`);
+}
+
/**
* Cannot use `%rune%` rune in non-runes mode
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 1bb7a69a20..9d79d88b23 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -952,7 +952,7 @@ const instance_script = {
const reassigned_bindings = bindings.filter((b) => b?.reassigned);
if (
- reassigned_bindings.length === 0 &&
+ node.body.expression.right.type !== 'Literal' &&
!bindings.some((b) => b?.kind === 'store_sub') &&
node.body.expression.left.type !== 'MemberExpression'
) {
@@ -1592,7 +1592,6 @@ function extract_type_and_comment(declarator, state, path) {
const comment_start = /** @type {any} */ (comment_node)?.start;
const comment_end = /** @type {any} */ (comment_node)?.end;
let comment = comment_node && str.original.substring(comment_start, comment_end);
-
if (comment_node) {
str.update(comment_start, comment_end, '');
}
@@ -1673,6 +1672,11 @@ function extract_type_and_comment(declarator, state, path) {
state.has_type_or_fallback = true;
const match = /@type {(.+)}/.exec(comment_node.value);
if (match) {
+ // try to find JSDoc comments after a hyphen `-`
+ const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value);
+ if (jsdoc_comment) {
+ cleaned_comment += jsdoc_comment[1]?.trim();
+ }
return {
type: match[1],
comment: cleaned_comment,
@@ -1693,7 +1697,6 @@ function extract_type_and_comment(declarator, state, path) {
};
}
}
-
return {
type: 'any',
comment: state.uses_ts ? comment : cleaned_comment,
diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
index 09eb0bfa68..4ff6a782b4 100644
--- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
+++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
@@ -24,6 +24,7 @@ const visitors = {
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
+ delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
},
@@ -94,6 +95,9 @@ const visitors = {
TSTypeAliasDeclaration() {
return b.empty;
},
+ TSTypeAssertion(node, context) {
+ return context.visit(node.expression);
+ },
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
index cd72d73005..28b314cdd5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
@@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) {
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
}
-// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
-// BSD Licensed
+// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
+// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
index ed22838582..362ac9dcad 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
@@ -133,7 +133,13 @@ const css_visitors = {
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
- if (node.selectors.length === 1) {
+ if (
+ node.selectors.length >= 1 &&
+ node.selectors.every(
+ (selector) =>
+ selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
+ )
+ ) {
const first = node.selectors[0];
node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index fc8108e46e..fbe6ca1cd3 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -1,13 +1,21 @@
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
-import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js';
+import {
+ get_parent_rules,
+ get_possible_values,
+ is_outer_global,
+ is_unscoped_pseudo_class
+} from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
+/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
+const FORWARD = 0;
+const BACKWARD = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
@@ -43,6 +51,27 @@ const nesting_selector = {
}
};
+/** @type {Compiler.AST.CSS.RelativeSelector} */
+const any_selector = {
+ type: 'RelativeSelector',
+ start: -1,
+ end: -1,
+ combinator: null,
+ selectors: [
+ {
+ type: 'TypeSelector',
+ name: '*',
+ start: -1,
+ end: -1
+ }
+ ],
+ metadata: {
+ is_global: false,
+ is_global_like: false,
+ scoped: false
+ }
+};
+
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set}
@@ -72,7 +101,8 @@ export function prune(stylesheet, element) {
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
- element
+ element,
+ BACKWARD
)
) {
node.metadata.used = true;
@@ -159,16 +189,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_selector(relative_selectors, rule, element) {
- const parent_selectors = relative_selectors.slice();
- const relative_selector = parent_selectors.pop();
+function apply_selector(relative_selectors, rule, element, direction) {
+ const rest_selectors = relative_selectors.slice();
+ const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched =
!!relative_selector &&
- relative_selector_might_apply_to_node(relative_selector, rule, element) &&
- apply_combinator(relative_selector, parent_selectors, rule, element);
+ relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
+ apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) {
if (!is_outer_global(relative_selector)) {
@@ -183,76 +214,67 @@ function apply_selector(relative_selectors, rule, element) {
/**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
- * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
+ * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_combinator(relative_selector, parent_selectors, rule, node) {
- if (!relative_selector.combinator) return true;
-
- const name = relative_selector.combinator.name;
+function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
+ const combinator =
+ direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
+ if (!combinator) return true;
- switch (name) {
+ switch (combinator.name) {
case ' ':
case '>': {
+ const is_adjacent = combinator.name === '>';
+ const parents =
+ direction === FORWARD
+ ? get_descendant_elements(node, is_adjacent)
+ : get_ancestor_elements(node, is_adjacent);
let parent_matched = false;
- const path = node.metadata.path;
- let i = path.length;
-
- while (i--) {
- const parent = path[i];
-
- if (parent.type === 'SnippetBlock') {
- if (seen.has(parent)) {
- parent_matched = true;
- } else {
- seen.add(parent);
-
- for (const site of parent.metadata.sites) {
- if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
- parent_matched = true;
- }
- }
- }
-
- break;
- }
-
- if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
- if (apply_selector(parent_selectors, rule, parent)) {
- parent_matched = true;
- }
-
- if (name === '>') return parent_matched;
+ for (const parent of parents) {
+ if (apply_selector(rest_selectors, rule, parent, direction)) {
+ parent_matched = true;
}
}
- return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
+ return (
+ parent_matched ||
+ (direction === BACKWARD &&
+ (!is_adjacent || parents.length === 0) &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
+ );
}
case '+':
case '~': {
- const siblings = get_possible_element_siblings(node, name === '+');
+ const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
- if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
+ if (
+ possible_sibling.type === 'RenderTag' ||
+ possible_sibling.type === 'SlotElement' ||
+ possible_sibling.type === 'Component'
+ ) {
// `{@render foo()}
foo
` with `:global(.x) + p` is a match
- if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
+ if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true;
}
- } else if (apply_selector(parent_selectors, rule, possible_sibling)) {
+ } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
- (get_element_parent(node) === null &&
- parent_selectors.every((selector) => is_global(selector, rule)))
+ (direction === BACKWARD &&
+ get_element_parent(node) === null &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
);
}
@@ -269,20 +291,26 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
* a global selector
* @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.AST.CSS.Rule} rule
+ * @returns {boolean}
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) {
return true;
}
+ let explicitly_global = false;
+
for (const s of selector.selectors) {
/** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null;
+ let can_be_global = false;
let owner = rule;
if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = s.args;
+ } else {
+ can_be_global = is_unscoped_pseudo_class(s);
}
}
@@ -291,18 +319,19 @@ function is_global(selector, rule) {
selector_list = owner.prelude;
}
- const has_global_selectors = selector_list?.children.some((complex_selector) => {
+ const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner)
);
});
+ explicitly_global ||= has_global_selectors;
- if (!has_global_selectors) {
+ if (!has_global_selectors && !can_be_global) {
return false;
}
}
- return true;
+ return explicitly_global || selector.selectors.length === 0;
}
const regex_backslash_and_following_character = /\\(.)/g;
@@ -313,9 +342,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function relative_selector_might_apply_to_node(relative_selector, rule, element) {
+function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
@@ -331,13 +361,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) {
- /** @type {Array} */
- const child_elements = [];
- /** @type {Array} */
- const descendant_elements = [];
- /** @type {Array} */
- let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
-
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
@@ -353,46 +376,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
)
)
);
- if (include_self) {
- child_elements.push(element);
- descendant_elements.push(element);
- }
-
- const seen = new Set();
-
- /**
- * @param {Compiler.AST.SvelteNode} node
- * @param {{ is_child: boolean }} state
- */
- function walk_children(node, state) {
- walk(node, state, {
- _(node, context) {
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- descendant_elements.push(node);
-
- if (context.state.is_child) {
- child_elements.push(node);
- context.state.is_child = false;
- context.next();
- context.state.is_child = true;
- } else {
- context.next();
- }
- } else if (node.type === 'RenderTag') {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- walk_children(snippet.body, context.state);
- }
- } else {
- context.next();
- }
- }
- });
- }
-
- walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@@ -403,37 +386,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let matched = false;
for (const complex_selector of complex_selectors) {
- const selectors = truncate(complex_selector);
- const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
- // In .x:has(> y), we want to search for y, ignoring the left-most combinator
- // (else it would try to walk further up and fail because there are no selectors left)
- if (selectors.length > 0) {
- selectors[0] = {
- ...selectors[0],
- combinator: null
- };
+ const [first, ...rest] = truncate(complex_selector);
+ // if it was just a :global(...)
+ if (!first) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ continue;
}
- const descendants =
- left_most_combinator.name === '+' || left_most_combinator.name === '~'
- ? (sibling_elements ??= get_following_sibling_elements(element, include_self))
- : left_most_combinator.name === '>'
- ? child_elements
- : descendant_elements;
-
- let selector_matched = false;
-
- // Iterate over all descendant elements and check if the selector inside :has matches
- for (const element of descendants) {
- if (
- selectors.length === 0 /* is :global(...) */ ||
- (element.metadata.scoped && selector_matched) ||
- apply_selector(selectors, rule, element)
- ) {
+ if (include_self) {
+ const selector_including_self = [
+ first.combinator ? { ...first, combinator: null } : first,
+ ...rest
+ ];
+ if (apply_selector(selector_including_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
- selector_matched = matched = true;
+ matched = true;
}
}
+
+ const selector_excluding_self = [
+ any_selector,
+ first.combinator ? first : { ...first, combinator: descendant_combinator },
+ ...rest
+ ];
+ if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ }
}
if (!matched) {
@@ -458,7 +438,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) {
const args = selector.args;
const complex_selector = args.children[0];
- return apply_selector(complex_selector.children, rule, element);
+ return apply_selector(complex_selector.children, rule, element, BACKWARD);
}
// We came across a :global, everything beyond it is global and therefore a potential match
@@ -507,7 +487,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
- } else if (apply_selector(relative, rule, element)) {
+ } else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true;
matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@@ -591,7 +571,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
for (const complex_selector of parent.prelude.children) {
if (
- apply_selector(get_relative_selectors(complex_selector), parent, element) ||
+ apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
@@ -612,80 +592,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true;
}
-/**
- * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
- * @param {boolean} include_self
- */
-function get_following_sibling_elements(element, include_self) {
- const path = element.metadata.path;
- let i = path.length;
-
- /** @type {Compiler.AST.SvelteNode} */
- let start = element;
- let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
- /** @type {Compiler.AST.Fragment} */ (path[0]).nodes
- );
-
- // find the set of nodes to walk...
- while (i--) {
- const node = path[i];
-
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- nodes = node.fragment.nodes;
- break;
- }
-
- if (node.type !== 'Fragment') {
- start = node;
- }
- }
-
- /** @type {Array} */
- const siblings = [];
-
- // ...then walk them, starting from the node containing the element in question
- // skipping nodes that appears before the element
-
- const seen = new Set();
- let skip = true;
-
- /** @param {Compiler.AST.SvelteNode} node */
- function get_siblings(node) {
- walk(node, null, {
- RegularElement(node) {
- if (node === element) {
- skip = false;
- if (include_self) siblings.push(node);
- } else if (!skip) {
- siblings.push(node);
- }
- },
- SvelteElement(node) {
- if (node === element) {
- skip = false;
- if (include_self) siblings.push(node);
- } else if (!skip) {
- siblings.push(node);
- }
- },
- RenderTag(node) {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- get_siblings(snippet.body);
- }
- }
- });
- }
-
- for (const node of nodes.slice(nodes.indexOf(start))) {
- get_siblings(node);
- }
-
- return siblings;
-}
-
/**
* @param {any} operator
* @param {any} expected_value
@@ -822,6 +728,84 @@ function unquote(str) {
return str;
}
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const ancestors = [];
+
+ const path = node.metadata.path;
+ let i = path.length;
+
+ while (i--) {
+ const parent = path[i];
+
+ if (parent.type === 'SnippetBlock') {
+ if (!seen.has(parent)) {
+ seen.add(parent);
+
+ for (const site of parent.metadata.sites) {
+ ancestors.push(...get_ancestor_elements(site, adjacent_only, seen));
+ }
+ }
+
+ break;
+ }
+
+ if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
+ ancestors.push(parent);
+ if (adjacent_only) {
+ break;
+ }
+ }
+ }
+
+ return ancestors;
+}
+
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_descendant_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const descendants = [];
+
+ /**
+ * @param {Compiler.AST.SvelteNode} node
+ */
+ function walk_children(node) {
+ walk(node, null, {
+ _(node, context) {
+ if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
+ descendants.push(node);
+
+ if (!adjacent_only) {
+ context.next();
+ }
+ } else if (node.type === 'RenderTag') {
+ for (const snippet of node.metadata.snippets) {
+ if (seen.has(snippet)) continue;
+
+ seen.add(snippet);
+ walk_children(snippet.body);
+ }
+ } else {
+ context.next();
+ }
+ }
+ });
+ }
+
+ walk_children(node.type === 'RenderTag' ? node : node.fragment);
+
+ return descendants;
+}
+
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
@@ -843,12 +827,13 @@ function get_element_parent(node) {
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set} seen
- * @returns {Map}
+ * @returns {Map}
*/
-function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
- /** @type {Map} */
+function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
+ /** @type {Map} */
const result = new Map();
const path = node.metadata.path;
@@ -859,9 +844,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
- let j = fragment.nodes.indexOf(current);
+ let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1);
- while (j--) {
+ while (j >= 0 && j < fragment.nodes.length) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
@@ -876,21 +861,32 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result;
}
}
- } else if (is_block(node)) {
- if (node.type === 'SlotElement') {
+ // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
+ // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (is_block(node) || node.type === 'Component') {
+ if (node.type === 'SlotElement' || node.type === 'Component') {
result.set(node, NODE_PROBABLY_EXISTS);
}
- const possible_last_child = get_possible_last_child(node, adjacent_only);
+ const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result);
- if (adjacent_only && has_definite_elements(possible_last_child)) {
+ if (
+ adjacent_only &&
+ node.type !== 'Component' &&
+ has_definite_elements(possible_last_child)
+ ) {
return result;
}
- } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
+ } else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS);
- // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
- // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (node.type === 'RenderTag') {
+ result.set(node, NODE_PROBABLY_EXISTS);
+ for (const snippet of node.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
+ }
}
+
+ j = direction === FORWARD ? j + 1 : j - 1;
}
current = path[i];
@@ -910,7 +906,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current);
for (const site of current.metadata.sites) {
- const siblings = get_possible_element_siblings(site, adjacent_only, seen);
+ const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
@@ -923,7 +919,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}{/each}` โ `` can be previous sibling of ``
- add_to_map(get_possible_last_child(current, adjacent_only), result);
+ add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result);
}
}
@@ -931,11 +927,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
}
/**
- * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
+ * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
+ * @param {Set} seen
* @returns {Map}
*/
-function get_possible_last_child(node, adjacent_only) {
+function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Array} */
let fragments = [];
@@ -956,12 +954,24 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement':
fragments.push(node.fragment);
break;
+
+ case 'SnippetBlock':
+ if (seen.has(node)) {
+ return new Map();
+ }
+ seen.add(node);
+ fragments.push(node.body);
+ break;
+
+ case 'Component':
+ fragments.push(node.fragment, ...[...node.metadata.snippets].map((s) => s.body));
+ break;
}
/** @type {Map} NodeMap */
const result = new Map();
- let exhaustive = node.type !== 'SlotElement';
+ let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) {
if (fragment == null) {
@@ -969,7 +979,7 @@ function get_possible_last_child(node, adjacent_only) {
continue;
}
- const map = loop_child(fragment.nodes, adjacent_only);
+ const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map);
add_to_map(map, result);
@@ -1012,27 +1022,28 @@ function add_to_map(from, to) {
}
/**
- * @param {NodeExistsValue | undefined} exist1
+ * @param {NodeExistsValue} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
- // @ts-expect-error TODO figure out if this is a bug
- if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
+ if (exist2 === undefined) return exist1;
return exist1 > exist2 ? exist1 : exist2;
}
/**
* @param {Compiler.AST.SvelteNode[]} children
+ * @param {Direction} direction
* @param {boolean} adjacent_only
+ * @param {Set} seen
*/
-function loop_child(children, adjacent_only) {
+function loop_child(children, direction, adjacent_only, seen) {
/** @type {Map} */
const result = new Map();
- let i = children.length;
+ let i = direction === FORWARD ? 0 : children.length - 1;
- while (i--) {
+ while (i >= 0 && i < children.length) {
const child = children[i];
if (child.type === 'RegularElement') {
@@ -1042,13 +1053,19 @@ function loop_child(children, adjacent_only) {
}
} else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS);
+ } else if (child.type === 'RenderTag') {
+ for (const snippet of child.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result);
+ }
} else if (is_block(child)) {
- const child_result = get_possible_last_child(child, adjacent_only);
+ const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
+
+ i = direction === FORWARD ? i + 1 : i - 1;
}
return result;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 1f636c32df..c62fb03e8f 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -432,6 +432,7 @@ export function analyze_component(root, source, options) {
uses_component_bindings: false,
uses_render_tags: false,
needs_context: false,
+ needs_mutation_validation: false,
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
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 561a004526..3ba81767cc 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -162,16 +162,8 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
- if (binding !== null && binding.initial !== null && !binding.updated) {
- const binding_type = binding.initial.type;
-
- if (
- binding_type === 'ArrowFunctionExpression' ||
- binding_type === 'FunctionDeclaration' ||
- binding_type === 'FunctionExpression'
- ) {
- target_function = binding.initial;
- }
+ if (binding?.is_function()) {
+ target_function = binding.initial;
}
}
@@ -191,6 +183,15 @@ function get_delegated_event(event_name, handler, context) {
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
+ // if the function access a snippet that can't be hoisted we bail out
+ if (
+ local_binding !== null &&
+ local_binding.initial?.type === 'SnippetBlock' &&
+ !local_binding.initial.metadata.can_hoist
+ ) {
+ return unhoisted;
+ }
+
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return unhoisted;
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 4d09d9293f..2eac934b33 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -17,6 +17,14 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
+ if (rune && rune !== '$inspect') {
+ for (const arg of node.arguments) {
+ if (arg.type === 'SpreadElement') {
+ e.rune_invalid_spread(node, rune);
+ }
+ }
+ }
+
switch (rune) {
case null:
if (!is_safe_identifier(node.callee, context.state.scope)) {
@@ -42,6 +50,9 @@ export function CallExpression(node, context) {
e.bindable_invalid_location(node);
}
+ // We need context in case the bound prop is stale
+ context.state.analysis.needs_context = true;
+
break;
case '$host':
@@ -114,7 +125,7 @@ export function CallExpression(node, context) {
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
- } else if (rune === '$state' && node.arguments.length > 1) {
+ } else if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
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 04f4347a40..d6c74eddb6 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
@@ -21,10 +21,6 @@ export function validate_assignment(node, argument, state) {
const binding = state.scope.get(argument.name);
if (state.analysis.runes) {
- if (binding?.kind === 'derived') {
- e.constant_assignment(node, 'derived state');
- }
-
if (binding?.node === state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
@@ -38,25 +34,6 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
- if (
- argument.type === 'MemberExpression' &&
- argument.object.type === 'ThisExpression' &&
- (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
- state.derived_state.some(
- (derived) =>
- derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name &&
- derived.private === (argument.property.type === 'PrivateIdentifier')
- )) ||
- (argument.property.type === 'Literal' &&
- argument.property.value &&
- typeof argument.property.value === 'string' &&
- state.derived_state.some(
- (derived) =>
- derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
- )))
- ) {
- e.constant_assignment(node, 'derived state');
- }
}
/**
@@ -81,7 +58,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (
- binding?.kind === 'derived' ||
binding?.declaration_kind === 'import' ||
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
) {
@@ -96,12 +72,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
- const thing =
- binding.declaration_kind === 'import'
- ? 'import'
- : binding.kind === 'derived'
- ? 'derived state'
- : 'constant';
+ const thing = binding.declaration_kind === 'import' ? 'import' : 'constant';
if (is_binding) {
e.constant_binding(node, thing);
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 cf5ba285cb..38fcee8d6f 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
@@ -219,7 +219,10 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(
- b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined))
+ b.const(
+ name,
+ b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined)
+ )
);
}
if (binding.kind === 'store_sub') {
@@ -390,6 +393,12 @@ export function client_component(analysis, options) {
);
}
+ if (analysis.needs_mutation_validation) {
+ component_block.body.unshift(
+ b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
+ );
+ }
+
const should_inject_context =
dev ||
analysis.needs_context ||
@@ -596,7 +605,7 @@ export function client_component(analysis, options) {
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
- prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
+ prop_def.reflect ? b.init('reflect', b.true) : undefined,
prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
].filter(Boolean)
)
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 63fe3223cf..243e1c64a3 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
@@ -30,7 +30,7 @@ export interface ClientTransformState extends TransformState {
/** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
- assign?: (node: Identifier, value: Expression) => Expression;
+ assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
index 421118cf68..a37ecd31cc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
@@ -1,10 +1,10 @@
-/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
-/** @import { AST, Binding } from '#compiler' */
+/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
+/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
-import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
+import { is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@@ -13,7 +13,8 @@ import {
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
-import { get_value } from './visitors/shared/declarations.js';
+import { walk } from 'zimmerframe';
+import { validate_mutation } from './visitors/shared/utils.js';
/**
* @param {Binding} binding
@@ -45,14 +46,6 @@ export function build_getter(node, state) {
return node;
}
-/**
- * @param {Expression} value
- * @param {Expression} previous
- */
-export function build_proxy_reassignment(value, previous) {
- return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
-}
-
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
@@ -118,6 +111,30 @@ function get_hoisted_params(node, context) {
}
}
}
+
+ if (dev) {
+ // this is a little hacky, but necessary for ownership validation
+ // to work inside hoisted event handlers
+
+ /**
+ * @param {AssignmentExpression | UpdateExpression} node
+ * @param {{ next: () => void, stop: () => void }} context
+ */
+ function visit(node, { next, stop }) {
+ if (validate_mutation(node, /** @type {any} */ (context), node) !== node) {
+ params.push(b.id('$$ownership_validator'));
+ stop();
+ } else {
+ next();
+ }
+ }
+
+ walk(/** @type {Node} */ (node), null, {
+ AssignmentExpression: visit,
+ UpdateExpression: visit
+ });
+ }
+
return params;
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
index 510f32cde5..2e051ec674 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
@@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js';
export function AnimateDirective(node, context) {
const expression =
node.expression === null
- ? b.literal(null)
+ ? b.null
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this
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 a8c615af93..4baa1c8e6c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -1,5 +1,4 @@
-/** @import { Location } from 'locate-character' */
-/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */
+/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
@@ -8,9 +7,10 @@ import {
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
-import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
-import { build_proxy_reassignment, should_proxy } from '../utils.js';
+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';
/**
* @param {AssignmentExpression} node
@@ -21,9 +21,7 @@ export function AssignmentExpression(node, context) {
visit_assignment_expression(node, context, build_assignment) ?? context.next()
);
- return is_ignored(node, 'ownership_invalid_mutation')
- ? b.call('$.skip_ownership_validation', b.thunk(expression))
- : expression;
+ return validate_mutation(node, context, expression);
}
/**
@@ -65,21 +63,12 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
- if (
+ const needs_proxy =
private_state.kind === 'state' &&
is_non_coercive_operator(operator) &&
- should_proxy(value, context.state.scope)
- ) {
- value = build_proxy_reassignment(value, b.member(b.this, private_state.id));
- }
-
- if (context.state.in_constructor) {
- // inside the constructor, we can assign to `this.#foo.v` rather than using `$.set`,
- // since nothing is tracking the signal at this point
- return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
- }
+ should_proxy(value, context.state.scope);
- return b.call('$.set', left, value);
+ return b.call('$.set', left, value, needs_proxy && b.true);
}
}
@@ -113,20 +102,18 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
- if (
+ return transform.assign(
+ object,
+ value,
!is_primitive &&
- binding.kind !== 'prop' &&
- binding.kind !== 'bindable_prop' &&
- binding.kind !== 'raw_state' &&
- binding.kind !== 'store_sub' &&
- context.state.analysis.runes &&
- should_proxy(right, context.state.scope) &&
- is_non_coercive_operator(operator)
- ) {
- value = build_proxy_reassignment(value, object);
- }
-
- return transform.assign(object, value);
+ binding.kind !== 'prop' &&
+ binding.kind !== 'bindable_prop' &&
+ binding.kind !== 'raw_state' &&
+ binding.kind !== 'store_sub' &&
+ context.state.analysis.runes &&
+ should_proxy(right, context.state.scope) &&
+ is_non_coercive_operator(operator)
+ );
}
// mutation
@@ -177,7 +164,9 @@ function build_assignment(operator, left, right, context) {
path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' &&
- (path.at(-3) === 'Component' || path.at(-3) === 'SvelteComponent'))
+ (path.at(-3) === 'Component' ||
+ path.at(-3) === 'SvelteComponent' ||
+ path.at(-3) === 'BindDirective'))
) {
should_transform = false;
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
index e0aef2d316..7588b24280 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
@@ -60,7 +60,7 @@ export function AwaitBlock(node, context) {
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
- : b.literal(null),
+ : b.null,
then_block,
catch_block
)
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
index c8c54a5a59..c563920855 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
@@ -16,7 +16,7 @@ export function BinaryExpression(node, context) {
'$.strict_equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
- operator === '!==' && b.literal(false)
+ operator === '!==' && b.false
);
}
@@ -25,7 +25,7 @@ export function BinaryExpression(node, context) {
'$.equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
- operator === '!=' && b.literal(false)
+ operator === '!=' && b.false
);
}
}
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 ed800e5226..efc3c95c3c 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
@@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
-import { build_proxy_reassignment, should_proxy } from '../utils.js';
+import { should_proxy } from '../utils.js';
/**
* @param {ClassBody} node
@@ -142,39 +142,17 @@ export function ClassBody(node, context) {
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
- if (field.kind === 'state') {
- // set foo(value) { this.#foo = value; }
- const value = b.id('value');
- const prev = b.member(b.this, field.id);
-
- body.push(
- b.method(
- 'set',
- definition.key,
- [value],
- [b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))]
- )
- );
- }
-
- if (field.kind === 'raw_state') {
- // set foo(value) { this.#foo = value; }
- const value = b.id('value');
- body.push(
- b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
- );
- }
+ // set foo(value) { this.#foo = value; }
+ const val = b.id('value');
- 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.method(
+ 'set',
+ definition.key,
+ [val],
+ [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
+ )
+ );
}
continue;
}
@@ -183,33 +161,6 @@ export function ClassBody(node, context) {
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
- if (dev && public_state.size > 0) {
- // add an `[$.ADD_OWNER]` method so that a class with state fields can widen ownership
- body.push(
- b.method(
- 'method',
- b.id('$.ADD_OWNER'),
- [b.id('owner')],
- [
- b.stmt(
- b.call(
- '$.add_owner_to_class',
- b.this,
- b.id('owner'),
- b.array(
- Array.from(public_state).map(([name]) =>
- b.thunk(b.call('$.get', b.member(b.this, b.private_id(name))))
- )
- ),
- is_ignored(node, 'ownership_invalid_binding') && b.true
- )
- )
- ],
- true
- )
- );
- }
-
return { ...node, body };
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
index 0876fa30b6..fdd21b2b7e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
@@ -40,9 +40,7 @@ export function IfBlock(node, context) {
b.if(
/** @type {Expression} */ (context.visit(node.test)),
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
- alternate_id
- ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false)))
- : undefined
+ alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)
])
)
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 501ecda555..3f2aada1f5 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
@@ -11,7 +11,9 @@ export function MemberExpression(node, context) {
if (node.property.type === 'PrivateIdentifier') {
const field = context.state.private_state.get(node.property.name);
if (field) {
- return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
+ return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === '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 9b3ecc922d..45a594af1f 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
@@ -689,7 +689,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
'=',
b.member(node_id, 'value'),
b.conditional(
- b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)),
+ b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)),
b.literal(''), // render null/undefined values as empty string to support placeholder options
value
)
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
index fdd705e32e..c6f4ba1ed3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
@@ -59,7 +59,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
- ? b.literal(null)
+ ? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
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 13c1b4bc51..63c03b0eb6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
@@ -1,8 +1,8 @@
/** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
-import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
+import { validate_mutation } from './shared/utils.js';
/**
* @param {UpdateExpression} node
@@ -51,7 +51,5 @@ export function UpdateExpression(node, context) {
);
}
- return is_ignored(node, 'ownership_invalid_mutation')
- ? b.call('$.skip_ownership_validation', b.thunk(update))
- : update;
+ return validate_mutation(node, context, update);
}
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 31e712cdcc..3a914fb560 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
@@ -116,8 +116,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
- const value =
- args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
+ const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
if (rune === '$state' || rune === '$state.raw') {
/**
@@ -300,7 +299,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
return [
b.declarator(
declarator.id,
- b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
+ b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
)
];
}
@@ -315,7 +314,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
return b.declarator(
path.node,
binding?.kind === 'state'
- ? b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
+ ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
: value
);
})
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 2bae4486dc..2ea68e206e 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
@@ -179,19 +179,29 @@ export function build_component(node, component_name, context, anchor = context.
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
- if (dev && attribute.name !== 'this') {
- binding_initializers.push(
- b.stmt(
- b.call(
- b.id('$.add_owner_effect'),
- expression.type === 'SequenceExpression'
- ? expression.expressions[0]
- : b.thunk(expression),
- b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ if (
+ dev &&
+ attribute.name !== 'this' &&
+ !is_ignored(node, 'ownership_invalid_binding') &&
+ // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation
+ attribute.expression.type !== 'SequenceExpression'
+ ) {
+ const left = object(attribute.expression);
+ const binding = left && context.state.scope.get(left.name);
+
+ if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') {
+ context.state.analysis.needs_mutation_validation = true;
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ '$$ownership_validator.binding',
+ b.literal(binding.node.name),
+ b.id(component_name),
+ b.thunk(expression)
+ )
)
- )
- );
+ );
+ }
}
if (expression.type === 'SequenceExpression') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
index 0bd8c352f6..a13ecfed2c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
@@ -24,8 +24,8 @@ export function add_state_transformers(context) {
) {
context.state.transform[name] = {
read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value,
- assign: (node, value) => {
- let call = b.call('$.set', node, value);
+ assign: (node, value, proxy = false) => {
+ let call = b.call('$.set', node, value, proxy && b.true);
if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') {
call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores'));
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
index 084c1e7c67..97cec7a729 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
@@ -95,7 +95,7 @@ export function build_set_attributes(
const call = b.call(
'$.set_attributes',
element_id,
- is_dynamic ? attributes_id : b.literal(null),
+ is_dynamic ? attributes_id : b.null,
b.object(values),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
@@ -120,7 +120,7 @@ export function build_set_attributes(
*/
export function build_attribute_value(value, context, memoize = (value) => value) {
if (value === true) {
- return { value: b.literal(true), has_state: false };
+ return { value: b.true, has_state: false };
}
if (!Array.isArray(value) || value.length === 1) {
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 df6308d631..af6e56f70c 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
@@ -1,13 +1,13 @@
-/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
+/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
-/** @import { ComponentClientTransformState } from '../../types' */
+/** @import { ComponentClientTransformState, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
-import { locator } from '../../../../../state.js';
+import { dev, is_ignored, locator } from '../../../../../state.js';
import { create_derived } from '../../utils.js';
/**
@@ -295,3 +295,60 @@ export function validate_binding(state, binding, expression) {
)
);
}
+
+/**
+ * In dev mode validate mutations to props
+ * @param {AssignmentExpression | UpdateExpression} node
+ * @param {Context} context
+ * @param {Expression} expression
+ */
+export function validate_mutation(node, context, expression) {
+ let left = /** @type {Expression | Super} */ (
+ node.type === 'AssignmentExpression' ? node.left : node.argument
+ );
+
+ if (!dev || left.type !== 'MemberExpression' || is_ignored(node, 'ownership_invalid_mutation')) {
+ return expression;
+ }
+
+ const name = object(left);
+ if (!name) return expression;
+
+ const binding = context.state.scope.get(name.name);
+ if (binding?.kind !== 'prop' && binding?.kind !== 'bindable_prop') return expression;
+
+ const state = /** @type {ComponentClientTransformState} */ (context.state);
+ state.analysis.needs_mutation_validation = true;
+
+ /** @type {Array} */
+ const path = [];
+
+ while (left.type === 'MemberExpression') {
+ if (left.property.type === 'Literal') {
+ path.unshift(left.property);
+ } else if (left.property.type === 'Identifier') {
+ if (left.computed) {
+ path.unshift(left.property);
+ } else {
+ path.unshift(b.literal(left.property.name));
+ }
+ } else {
+ return expression;
+ }
+
+ left = left.object;
+ }
+
+ path.unshift(b.literal(name.name));
+
+ const loc = locator(/** @type {number} */ (left.start));
+
+ return b.call(
+ '$$ownership_validator.mutation',
+ b.literal(binding.prop_alias),
+ b.array(path),
+ expression,
+ loc && b.literal(loc.line),
+ loc && b.literal(loc.column)
+ );
+}
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 df3d831d3c..f746e90fe2 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
@@ -186,12 +186,10 @@ export function server_component(analysis, options) {
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
- b.stmt(
- b.function(
- b.id('$$render_inner'),
- [b.id('$$payload')],
- b.block(/** @type {Statement[]} */ (rest))
- )
+ b.function_declaration(
+ b.id('$$render_inner'),
+ [b.id('$$payload')],
+ b.block(/** @type {Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
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 386c6b6ff3..a425bc5ec4 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
@@ -13,11 +13,11 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
- return b.id('undefined');
+ return b.void0;
}
if (rune === '$effect.tracking') {
- return b.literal(false);
+ return b.false;
}
if (rune === '$effect.root') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
index 7ece04ae3d..e7925071cd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
@@ -38,7 +38,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
- ? b.literal(null)
+ ? b.null
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
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 c4c31d7eb3..a9c9777335 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
@@ -45,7 +45,7 @@ export function VariableDeclaration(node, context) {
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
- : b.id('undefined');
+ : b.void0;
return b.assignment_pattern(node.left, right);
}
}
@@ -75,8 +75,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
- const value =
- args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
+ const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
if (rune === '$derived.by') {
declarations.push(
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index a5227c1b51..b6063c3234 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -1,4 +1,4 @@
-/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
+/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
@@ -80,19 +80,23 @@ export class Binding {
return this.mutated || this.reassigned;
}
+ /**
+ * @returns {this is Binding & { initial: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression }}
+ */
is_function() {
- if (this.reassigned) {
+ if (this.updated) {
// even if it's reassigned to another function,
// we can't use it directly as e.g. an event handler
return false;
}
- if (this.declaration_kind === 'function') {
- return true;
- }
-
const type = this.initial?.type;
- return type === 'ArrowFunctionExpression' || type === 'FunctionExpression';
+
+ return (
+ type === 'ArrowFunctionExpression' ||
+ type === 'FunctionExpression' ||
+ type === 'FunctionDeclaration'
+ );
}
}
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index abe2b115de..f09b881303 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -53,6 +53,7 @@ export interface ComponentAnalysis extends Analysis {
uses_component_bindings: boolean;
uses_render_tags: boolean;
needs_context: boolean;
+ needs_mutation_validation: boolean;
needs_props: boolean;
/** Set to the first event directive (on:x) found on a DOM element in the code */
event_directive_node: AST.OnDirective | null;
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index ecb595d74d..736738d19f 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -154,6 +154,8 @@ export function unary(operator, argument) {
return { type: 'UnaryExpression', argument, operator, prefix: true };
}
+export const void0 = unary('void', literal(0));
+
/**
* @param {ESTree.Expression} test
* @param {ESTree.Expression} consequent
@@ -483,7 +485,7 @@ export function do_while(test, body) {
const true_instance = literal(true);
const false_instance = literal(false);
-const null_instane = literal(null);
+const null_instance = literal(null);
/** @type {ESTree.DebuggerStatement} */
const debugger_builder = {
@@ -645,7 +647,7 @@ export {
return_builder as return,
if_builder as if,
this_instance as this,
- null_instane as null,
+ null_instance as null,
debugger_builder as debugger
};
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index a4840ce4eb..7e5196c606 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -20,8 +20,8 @@ export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;
+export const EFFECT_IS_UPDATING = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
-export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js
index bd94d5ad8a..7a2fdd0edb 100644
--- a/packages/svelte/src/internal/client/context.js
+++ b/packages/svelte/src/internal/client/context.js
@@ -1,7 +1,6 @@
/** @import { ComponentContext } from '#client' */
import { DEV } from 'esm-env';
-import { add_owner } from './dev/ownership.js';
import { lifecycle_outside_component } from '../shared/errors.js';
import { source } from './reactivity/sources.js';
import {
@@ -11,7 +10,7 @@ import {
set_active_reaction,
untrack
} from './runtime.js';
-import { effect } from './reactivity/effects.js';
+import { effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
/** @type {ComponentContext | null} */
@@ -67,15 +66,6 @@ export function getContext(key) {
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
-
- if (DEV) {
- // When state is put into context, we treat as if it's global from now on.
- // We do for performance reasons (it's for example very expensive to call
- // getContext on a big object many times when part of a list component)
- // and danger of false positives.
- untrack(() => add_owner(context, null, true));
- }
-
context_map.set(key, context);
return context;
}
@@ -112,15 +102,16 @@ export function getAllContexts() {
* @returns {void}
*/
export function push(props, runes = false, fn) {
- component_context = {
+ var ctx = (component_context = {
p: component_context,
c: null,
+ d: false,
e: null,
m: false,
s: props,
x: null,
l: null
- };
+ });
if (legacy_mode_flag && !runes) {
component_context.l = {
@@ -131,6 +122,10 @@ export function push(props, runes = false, fn) {
};
}
+ teardown(() => {
+ /** @type {ComponentContext} */ (ctx).d = true;
+ });
+
if (DEV) {
// component function
component_context.function = fn;
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index 62119b36db..6c40a744df 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -1,12 +1,11 @@
-/** @import { ProxyMetadata } from '#client' */
/** @typedef {{ file: string, line: number, column: number }} Location */
-import { STATE_SYMBOL_METADATA } from '../constants.js';
-import { render_effect, user_pre_effect } from '../reactivity/effects.js';
-import { dev_current_component_function } from '../context.js';
-import { get_prototype_of } from '../../shared/utils.js';
+import { get_descriptor } from '../../shared/utils.js';
+import { LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
+import { FILENAME } from '../../../constants.js';
+import { component_context } from '../context.js';
import * as w from '../warnings.js';
-import { FILENAME, UNINITIALIZED } from '../../../constants.js';
+import { sanitize_location } from '../../../utils.js';
/** @type {Record>} */
const boundaries = {};
@@ -71,8 +70,6 @@ export function get_component() {
return null;
}
-export const ADD_OWNER = Symbol('ADD_OWNER');
-
/**
* Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
* such that subsequent calls to `get_component` can tell us which component is responsible
@@ -108,197 +105,69 @@ export function mark_module_end(component) {
}
/**
- * @param {any} object
- * @param {any | null} owner
- * @param {boolean} [global]
- * @param {boolean} [skip_warning]
+ * Sets up a validator that
+ * - traverses the path of a prop to find out if it is allowed to be mutated
+ * - checks that the binding chain is not interrupted
+ * @param {Record} props
*/
-export function add_owner(object, owner, global = false, skip_warning = false) {
- if (object && !global) {
- const component = dev_current_component_function;
- const metadata = object[STATE_SYMBOL_METADATA];
- if (metadata && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) {
- w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
+export function create_ownership_validator(props) {
+ const component = component_context?.function;
+ const parent = component_context?.p?.function;
+
+ return {
+ /**
+ * @param {string} prop
+ * @param {any[]} path
+ * @param {any} result
+ * @param {number} line
+ * @param {number} column
+ */
+ mutation: (prop, path, result, line, column) => {
+ const name = path[0];
+ if (is_bound(props, name) || !parent) {
+ return result;
}
- }
- }
- add_owner_to_object(object, owner, new Set());
-}
-
-/**
- * @param {() => unknown} get_object
- * @param {any} Component
- * @param {boolean} [skip_warning]
- */
-export function add_owner_effect(get_object, Component, skip_warning = false) {
- user_pre_effect(() => {
- add_owner(get_object(), Component, false, skip_warning);
- });
-}
-
-/**
- * @param {any} _this
- * @param {Function} owner
- * @param {Array<() => any>} getters
- * @param {boolean} skip_warning
- */
-export function add_owner_to_class(_this, owner, getters, skip_warning) {
- _this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
-
- for (let i = 0; i < getters.length; i += 1) {
- const current = getters[i]();
- // For performance reasons we only re-add the owner if the state has changed
- if (current !== _this[ADD_OWNER][i]) {
- _this[ADD_OWNER].current[i] = current;
- add_owner(current, owner, false, skip_warning);
- }
- }
-}
-
-/**
- * @param {ProxyMetadata | null} from
- * @param {ProxyMetadata} to
- */
-export function widen_ownership(from, to) {
- if (to.owners === null) {
- return;
- }
-
- while (from) {
- if (from.owners === null) {
- to.owners = null;
- break;
- }
-
- for (const owner of from.owners) {
- to.owners.add(owner);
- }
-
- from = from.parent;
- }
-}
-
-/**
- * @param {any} object
- * @param {Function | null} owner If `null`, then the object is globally owned and will not be checked
- * @param {Set} seen
- */
-function add_owner_to_object(object, owner, seen) {
- const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL_METADATA]);
+ let value = props[name];
- if (metadata) {
- // this is a state proxy, add owner directly, if not globally shared
- if ('owners' in metadata && metadata.owners != null) {
- if (owner) {
- metadata.owners.add(owner);
- } else {
- metadata.owners = null;
+ for (let i = 1; i < path.length - 1; i++) {
+ if (!value?.[STATE_SYMBOL]) {
+ return result;
+ }
+ value = value[path[i]];
}
- }
- } else if (object && typeof object === 'object') {
- if (seen.has(object)) return;
- seen.add(object);
- if (ADD_OWNER in object && object[ADD_OWNER]) {
- // this is a class with state fields. we put this in a render effect
- // so that if state is replaced (e.g. `instance.name = { first, last }`)
- // the new state is also co-owned by the caller of `getContext`
- render_effect(() => {
- object[ADD_OWNER](owner);
- });
- } else {
- var proto = get_prototype_of(object);
- if (proto === Object.prototype) {
- // recurse until we find a state proxy
- for (const key in object) {
- if (Object.getOwnPropertyDescriptor(object, key)?.get) {
- // Similar to the class case; the getter could update with a new state
- let current = UNINITIALIZED;
- render_effect(() => {
- const next = object[key];
- if (current !== next) {
- current = next;
- add_owner_to_object(next, owner, seen);
- }
- });
- } else {
- add_owner_to_object(object[key], owner, seen);
- }
- }
- } else if (proto === Array.prototype) {
- // recurse until we find a state proxy
- for (let i = 0; i < object.length; i += 1) {
- add_owner_to_object(object[i], owner, seen);
- }
+ const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`);
+
+ w.ownership_invalid_mutation(name, location, prop, parent[FILENAME]);
+
+ return result;
+ },
+ /**
+ * @param {any} key
+ * @param {any} child_component
+ * @param {() => any} value
+ */
+ binding: (key, child_component, value) => {
+ if (!is_bound(props, key) && parent && value()?.[STATE_SYMBOL]) {
+ w.ownership_invalid_binding(
+ component[FILENAME],
+ key,
+ child_component[FILENAME],
+ parent[FILENAME]
+ );
}
}
- }
+ };
}
/**
- * @param {ProxyMetadata} metadata
- * @param {Function} component
- * @returns {boolean}
+ * @param {Record} props
+ * @param {string} prop_name
*/
-function has_owner(metadata, component) {
- if (metadata.owners === null) {
- return true;
- }
-
- return (
- metadata.owners.has(component) ||
- // This helps avoid false positives when using HMR, where the component function is replaced
- (FILENAME in component &&
- [...metadata.owners].some(
- (owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
- )) ||
- (metadata.parent !== null && has_owner(metadata.parent, component))
- );
-}
-
-/**
- * @param {ProxyMetadata} metadata
- * @returns {any}
- */
-function get_owner(metadata) {
- return (
- metadata?.owners?.values().next().value ??
- get_owner(/** @type {ProxyMetadata} */ (metadata.parent))
- );
-}
-
-let skip = false;
-
-/**
- * @param {() => any} fn
- */
-export function skip_ownership_validation(fn) {
- skip = true;
- fn();
- skip = false;
-}
-
-/**
- * @param {ProxyMetadata} metadata
- */
-export function check_ownership(metadata) {
- if (skip) return;
-
- const component = get_component();
-
- if (component && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- // @ts-expect-error
- if (original[FILENAME] !== component[FILENAME]) {
- // @ts-expect-error
- w.ownership_invalid_mutation(component[FILENAME], original[FILENAME]);
- } else {
- w.ownership_invalid_mutation();
- }
- }
+function is_bound(props, prop_name) {
+ // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
+ // or `createClassComponent(Component, props)`
+ const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
+ return !!get_descriptor(props, prop_name)?.set || (is_entry_props && prop_name in props);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js
index ecbfcbc010..fc081b8956 100644
--- a/packages/svelte/src/internal/client/dom/elements/class.js
+++ b/packages/svelte/src/internal/client/dom/elements/class.js
@@ -14,7 +14,11 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
- if (hydrating || prev !== value) {
+ if (
+ hydrating ||
+ prev !== value ||
+ prev === undefined // for edge case of `class={undefined}`
+ ) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 0ad9045b20..aae44d4b39 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
-import { get_descriptor } from '../../shared/utils.js';
+import { get_descriptor, is_extensible } from '../../shared/utils.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -34,26 +34,31 @@ export function init_operations() {
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
+ var text_prototype = Text.prototype;
// @ts-ignore
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
// @ts-ignore
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
- // the following assignments improve perf of lookups on DOM nodes
- // @ts-expect-error
- element_prototype.__click = undefined;
- // @ts-expect-error
- element_prototype.__className = undefined;
- // @ts-expect-error
- element_prototype.__attributes = null;
- // @ts-expect-error
- element_prototype.__style = undefined;
- // @ts-expect-error
- element_prototype.__e = undefined;
-
- // @ts-expect-error
- Text.prototype.__t = undefined;
+ if (is_extensible(element_prototype)) {
+ // the following assignments improve perf of lookups on DOM nodes
+ // @ts-expect-error
+ element_prototype.__click = undefined;
+ // @ts-expect-error
+ element_prototype.__className = undefined;
+ // @ts-expect-error
+ element_prototype.__attributes = null;
+ // @ts-expect-error
+ element_prototype.__style = undefined;
+ // @ts-expect-error
+ element_prototype.__e = undefined;
+ }
+
+ if (is_extensible(text_prototype)) {
+ // @ts-expect-error
+ text_prototype.__t = undefined;
+ }
if (DEV) {
// @ts-expect-error
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 682816e1d6..8a5b5033a7 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -307,21 +307,6 @@ export function state_prototype_fixed() {
}
}
-/**
- * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
- * @returns {never}
- */
-export function state_unsafe_local_read() {
- if (DEV) {
- const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`);
-
- error.name = 'Svelte error';
- throw error;
- } else {
- throw new Error(`https://svelte.dev/e/state_unsafe_local_read`);
- }
-}
-
/**
* Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 31da00dbb4..7eed8a744a 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -4,15 +4,7 @@ export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
-export {
- ADD_OWNER,
- add_owner,
- mark_module_start,
- mark_module_end,
- add_owner_effect,
- add_owner_to_class,
- skip_ownership_validation
-} from './dev/ownership.js';
+export { mark_module_start, mark_module_end, create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
@@ -101,7 +93,7 @@ export {
text,
props_id
} from './dom/template.js';
-export { derived, derived_safe_equal } from './reactivity/deriveds.js';
+export { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
effect_tracking,
effect_root,
@@ -113,7 +105,7 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
-export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js';
+export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
export {
prop,
rest_props,
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 4c262880f1..5e0aa3dbc3 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,7 +1,6 @@
-/** @import { ProxyMetadata, Source } from '#client' */
+/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
-import { get, active_effect } from './runtime.js';
-import { component_context } from './context.js';
+import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
import {
array_prototype,
get_descriptor,
@@ -9,9 +8,8 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
-import { check_ownership, widen_ownership } from './dev/ownership.js';
-import { source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
+import { state as source, set } from './reactivity/sources.js';
+import { STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
@@ -20,16 +18,9 @@ import { tracing_mode_flag } from '../flags/index.js';
/**
* @template T
* @param {T} value
- * @param {ProxyMetadata | null} [parent]
- * @param {Source} [prev] dev mode only
* @returns {T}
*/
-export function proxy(value, parent = null, prev) {
- /** @type {Error | null} */
- var stack = null;
- if (DEV && tracing_mode_flag) {
- stack = get_stack('CreatedAt');
- }
+export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
@@ -46,37 +37,30 @@ export function proxy(value, parent = null, prev) {
var is_proxied_array = is_array(value);
var version = source(0);
+ var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
+ var reaction = active_reaction;
+
+ /**
+ * @template T
+ * @param {() => T} fn
+ */
+ var with_parent = (fn) => {
+ var previous_reaction = active_reaction;
+ set_active_reaction(reaction);
+
+ /** @type {T} */
+ var result = fn();
+
+ set_active_reaction(previous_reaction);
+ return result;
+ };
+
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
}
- /** @type {ProxyMetadata} */
- var metadata;
-
- if (DEV) {
- metadata = {
- parent,
- owners: null
- };
-
- if (prev) {
- // Reuse owners from previous state; necessary because reassignment is not guaranteed to have correct component context.
- // If no previous proxy exists we play it safe and assume ownerless state
- // @ts-expect-error
- const prev_owners = prev.v?.[STATE_SYMBOL_METADATA]?.owners;
- metadata.owners = prev_owners ? new Set(prev_owners) : null;
- } else {
- metadata.owners =
- parent === null
- ? component_context !== null
- ? new Set([component_context.function])
- : null
- : new Set();
- }
- }
-
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
@@ -95,10 +79,13 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop);
if (s === undefined) {
- s = source(descriptor.value, stack);
+ s = with_parent(() => source(descriptor.value, stack));
sources.set(prop, s);
} else {
- set(s, proxy(descriptor.value, metadata));
+ set(
+ s,
+ with_parent(() => proxy(descriptor.value))
+ );
}
return true;
@@ -109,7 +96,10 @@ export function proxy(value, parent = null, prev) {
if (s === undefined) {
if (prop in target) {
- sources.set(prop, source(UNINITIALIZED, stack));
+ sources.set(
+ prop,
+ with_parent(() => source(UNINITIALIZED, stack))
+ );
}
} else {
// When working with arrays, we need to also ensure we update the length when removing
@@ -130,10 +120,6 @@ export function proxy(value, parent = null, prev) {
},
get(target, prop, receiver) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return metadata;
- }
-
if (prop === STATE_SYMBOL) {
return value;
}
@@ -143,28 +129,12 @@ export function proxy(value, parent = null, prev) {
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
- s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
+ s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack));
sources.set(prop, s);
}
if (s !== undefined) {
var v = get(s);
-
- // In case of something like `foo = bar.map(...)`, foo would have ownership
- // of the array itself, while the individual items would have ownership
- // of the component that created bar. That means if we later do `foo[0].baz = 42`,
- // we could get a false-positive ownership violation, since the two proxies
- // are not connected to each other via the parent metadata relationship.
- // For this reason, we need to widen the ownership of the children
- // upon access when we detect they are not connected.
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = v?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- }
-
return v === UNINITIALIZED ? undefined : v;
}
@@ -195,10 +165,6 @@ export function proxy(value, parent = null, prev) {
},
has(target, prop) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return true;
- }
-
if (prop === STATE_SYMBOL) {
return true;
}
@@ -211,7 +177,7 @@ export function proxy(value, parent = null, prev) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
- s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
+ s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack));
sources.set(prop, s);
}
@@ -238,7 +204,7 @@ export function proxy(value, parent = null, prev) {
// If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
- other_s = source(UNINITIALIZED, stack);
+ other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s);
}
}
@@ -250,22 +216,19 @@ export function proxy(value, parent = null, prev) {
// object property before writing to that property.
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
- s = source(undefined, stack);
- set(s, proxy(value, metadata));
+ s = with_parent(() => source(undefined, stack));
+ set(
+ s,
+ with_parent(() => proxy(value))
+ );
sources.set(prop, s);
}
} else {
has = s.v !== UNINITIALIZED;
- set(s, proxy(value, metadata));
- }
-
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = value?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- check_ownership(metadata);
+ set(
+ s,
+ with_parent(() => proxy(value))
+ );
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
@@ -330,8 +293,18 @@ function update_version(signal, d = 1) {
* @param {any} value
*/
export function get_proxied_value(value) {
- if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
- return value[STATE_SYMBOL];
+ try {
+ if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
+ return value[STATE_SYMBOL];
+ }
+ } catch {
+ // the above if check can throw an error if the value in question
+ // is the contentWindow of an iframe on another domain, in which
+ // case we want to just return the value (because it's definitely
+ // not a proxied value) so we don't break any JavaScript interacting
+ // with that iframe (such as various payment companies client side
+ // JavaScript libraries interacting with their iframes on the same
+ // domain)
}
return value;
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 795417cc0f..c9a8f7674a 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -8,7 +8,8 @@ import {
skip_reaction,
update_reaction,
increment_write_version,
- set_active_effect
+ set_active_effect,
+ push_reaction_value
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@@ -61,6 +62,20 @@ export function derived(fn) {
return signal;
}
+/**
+ * @template V
+ * @param {() => V} fn
+ * @returns {Derived}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function user_derived(fn) {
+ const d = derived(fn);
+
+ push_reaction_value(d);
+
+ return d;
+}
+
/**
* @template V
* @param {() => V} fn
@@ -116,7 +131,7 @@ function get_derived_parent_effect(derived) {
* @param {Derived} derived
* @returns {T}
*/
-function execute_derived(derived) {
+export function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 5a3b30281f..bd85b14df0 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -1,4 +1,4 @@
-/** @import { Source } from './types.js' */
+/** @import { Derived, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
@@ -10,24 +10,10 @@ import {
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
-import {
- active_effect,
- get,
- captured_signals,
- set_active_effect,
- untrack,
- active_reaction,
- set_active_reaction
-} from '../runtime.js';
+import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
-import {
- BRANCH_EFFECT,
- LEGACY_DERIVED_PROP,
- LEGACY_PROPS,
- ROOT_EFFECT,
- STATE_SYMBOL
-} from '../constants.js';
+import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@@ -249,6 +235,14 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
+/**
+ * @param {Derived} current_value
+ * @returns {boolean}
+ */
+function has_destroyed_component_ctx(current_value) {
+ return current_value.ctx?.d ?? false;
+}
+
/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@@ -382,6 +376,11 @@ export function prop(props, key, flags, fallback) {
return (inner_current_value.v = parent_value);
});
+ // Ensure we eagerly capture the initial value if it's bindable
+ if (bindable) {
+ get(current_value);
+ }
+
if (!immutable) current_value.equals = safe_equals;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
@@ -408,11 +407,21 @@ export function prop(props, key, flags, fallback) {
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
+
+ if (has_destroyed_component_ctx(current_value)) {
+ return value;
+ }
+
untrack(() => get(current_value)); // force a synchronisation immediately
}
return value;
}
+
+ if (has_destroyed_component_ctx(current_value)) {
+ return current_value.v;
+ }
+
return get(current_value);
};
}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index f6a3fd7e33..cae49c1832 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -11,10 +11,12 @@ import {
untrack,
increment_write_version,
update_effect,
- derived_sources,
- set_derived_sources,
+ reaction_sources,
+ set_reaction_sources,
check_dirtiness,
- untracking
+ untracking,
+ is_destroying_effect,
+ push_reaction_value
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
@@ -32,8 +34,11 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
+import { proxy } from '../proxy.js';
+import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
+export const old_values = new Map();
/**
* @param {Set} v
@@ -48,6 +53,7 @@ export function set_inspect_effects(v) {
* @param {Error | null} [stack]
* @returns {Source}
*/
+// TODO rename this to `state` throughout the codebase
export function source(v, stack) {
/** @type {Value} */
var signal = {
@@ -70,9 +76,15 @@ export function source(v, stack) {
/**
* @template V
* @param {V} v
+ * @param {Error | null} [stack]
*/
-export function state(v) {
- return push_derived_source(source(v));
+/*#__NO_SIDE_EFFECTS__*/
+export function state(v, stack) {
+ const s = source(v, stack);
+
+ push_reaction_value(s);
+
+ return s;
}
/**
@@ -97,33 +109,6 @@ export function mutable_source(initial_value, immutable = false) {
return s;
}
-/**
- * @template V
- * @param {V} v
- * @param {boolean} [immutable]
- * @returns {Source}
- */
-export function mutable_state(v, immutable = false) {
- return push_derived_source(mutable_source(v, immutable));
-}
-
-/**
- * @template V
- * @param {Source} source
- */
-/*#__NO_SIDE_EFFECTS__*/
-function push_derived_source(source) {
- if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) {
- if (derived_sources === null) {
- set_derived_sources([source]);
- } else {
- derived_sources.push(source);
- }
- }
-
- return source;
-}
-
/**
* @template V
* @param {Value} source
@@ -141,22 +126,23 @@ export function mutate(source, value) {
* @template V
* @param {Source} source
* @param {V} value
+ * @param {boolean} [should_proxy]
* @returns {V}
*/
-export function set(source, value) {
+export function set(source, value, should_proxy = false) {
if (
active_reaction !== null &&
!untracking &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
- // If the source was created locally within the current derived, then
- // we allow the mutation.
- (derived_sources === null || !derived_sources.includes(source))
+ !reaction_sources?.includes(source)
) {
e.state_unsafe_mutation();
}
- return internal_set(source, value);
+ let new_value = should_proxy ? proxy(value) : value;
+
+ return internal_set(source, new_value);
}
/**
@@ -168,6 +154,13 @@ export function set(source, value) {
export function internal_set(source, value) {
if (!source.equals(value)) {
var old_value = source.v;
+
+ if (is_destroying_effect) {
+ old_values.set(source, value);
+ } else {
+ old_values.set(source, old_value);
+ }
+
source.v = value;
source.wv = increment_write_version();
@@ -179,6 +172,14 @@ export function internal_set(source, value) {
}
}
+ if ((source.f & DERIVED) !== 0) {
+ // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies
+ if ((source.f & DIRTY) !== 0) {
+ execute_derived(/** @type {Derived} */ (source));
+ }
+ set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
+ }
+
mark_reactions(source, DIRTY);
// It's possible that the current reaction might not have up-to-date dependencies
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index bbe4dc3d9b..a7662be617 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -22,11 +22,12 @@ import {
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
- BOUNDARY_EFFECT
+ BOUNDARY_EFFECT,
+ EFFECT_IS_UPDATING
} from './constants.js';
import { flush_tasks } from './dom/task.js';
-import { internal_set } from './reactivity/sources.js';
-import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
+import { internal_set, old_values } from './reactivity/sources.js';
+import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
import { tracing_mode_flag } from '../flags/index.js';
@@ -87,17 +88,28 @@ export function set_active_effect(effect) {
}
/**
- * When sources are created within a derived, we record them so that we can safely allow
- * local mutations to these sources without the side-effect error being invoked unnecessarily.
+ * When sources are created within a reaction, reading and writing
+ * them should not cause a re-run
* @type {null | Source[]}
*/
-export let derived_sources = null;
+export let reaction_sources = null;
/**
* @param {Source[] | null} sources
*/
-export function set_derived_sources(sources) {
- derived_sources = sources;
+export function set_reaction_sources(sources) {
+ reaction_sources = sources;
+}
+
+/** @param {Value} value */
+export function push_reaction_value(value) {
+ if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
+ if (reaction_sources === null) {
+ set_reaction_sources([value]);
+ } else {
+ reaction_sources.push(value);
+ }
+ }
}
/**
@@ -367,6 +379,9 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
+
+ if (reaction_sources?.includes(signal)) continue;
+
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
} else if (effect === reaction) {
@@ -391,9 +406,10 @@ export function update_reaction(reaction) {
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
- var prev_derived_sources = derived_sources;
+ var previous_reaction_sources = reaction_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
+
var flags = reaction.f;
new_deps = /** @type {null | Value[]} */ (null);
@@ -403,11 +419,13 @@ export function update_reaction(reaction) {
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
- derived_sources = null;
+ reaction_sources = null;
set_component_context(reaction.ctx);
untracking = false;
read_version++;
+ reaction.f |= EFFECT_IS_UPDATING;
+
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@@ -458,8 +476,16 @@ export function update_reaction(reaction) {
// we need to increment the read version to ensure that
// any dependencies in this reaction aren't marked with
// the same version
- if (previous_reaction !== null) {
+ if (previous_reaction !== reaction) {
read_version++;
+
+ if (untracked_writes !== null) {
+ if (previous_untracked_writes === null) {
+ previous_untracked_writes = untracked_writes;
+ } else {
+ previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes));
+ }
+ }
}
return result;
@@ -469,9 +495,11 @@ export function update_reaction(reaction) {
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
- derived_sources = prev_derived_sources;
+ reaction_sources = previous_reaction_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
+
+ reaction.f ^= EFFECT_IS_UPDATING;
}
}
@@ -664,6 +692,7 @@ function flush_queued_root_effects() {
var collected_effects = process_effects(root_effects[i]);
flush_queued_effects(collected_effects);
}
+ old_values.clear();
}
} finally {
is_flushing = false;
@@ -857,24 +886,23 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
- if (derived_sources !== null && derived_sources.includes(signal)) {
- e.state_unsafe_local_read();
- }
- var deps = active_reaction.deps;
- if (signal.rv < read_version) {
- signal.rv = read_version;
- // If the signal is accessing the same dependencies in the same
- // order as it did last time, increment `skipped_deps`
- // rather than updating `new_deps`, which creates GC cost
- if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
- skipped_deps++;
- } else if (new_deps === null) {
- new_deps = [signal];
- } else if (!skip_reaction || !new_deps.includes(signal)) {
- // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
- // an unowned derived because skip_reaction is true, then we need to ensure that
- // we don't have duplicates
- new_deps.push(signal);
+ if (!reaction_sources?.includes(signal)) {
+ var deps = active_reaction.deps;
+ if (signal.rv < read_version) {
+ signal.rv = read_version;
+ // If the signal is accessing the same dependencies in the same
+ // order as it did last time, increment `skipped_deps`
+ // rather than updating `new_deps`, which creates GC cost
+ if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
+ skipped_deps++;
+ } else if (new_deps === null) {
+ new_deps = [signal];
+ } else if (!skip_reaction || !new_deps.includes(signal)) {
+ // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
+ // an unowned derived because skip_reaction is true, then we need to ensure that
+ // we don't have duplicates
+ new_deps.push(signal);
+ }
}
}
} else if (
@@ -923,6 +951,10 @@ export function get(signal) {
}
}
+ if (is_destroying_effect && old_values.has(signal)) {
+ return old_values.get(signal);
+ }
+
return signal.v;
}
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 7208ed7783..b46bdf2013 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -14,6 +14,8 @@ export type ComponentContext = {
p: null | ComponentContext;
/** context */
c: null | Map;
+ /** destroyed */
+ d: boolean;
/** deferred effects */
e: null | Array<{
fn: () => void | (() => void);
@@ -177,14 +179,6 @@ export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
-/** Dev-only */
-export interface ProxyMetadata {
- /** The components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */
- owners: null | Set;
- /** The parent metadata object */
- parent: null | ProxyMetadata;
-}
-
export type ProxyStateObject> = T & {
[STATE_SYMBOL]: T;
};
diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 250c6eca2f..c84b487e28 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -129,27 +129,30 @@ export function lifecycle_double_unmount() {
}
/**
- * %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+ * %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
* @param {string} parent
+ * @param {string} prop
* @param {string} child
* @param {string} owner
*/
-export function ownership_invalid_binding(parent, child, owner) {
+export function ownership_invalid_binding(parent, prop, child, owner) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed a value to ${child} with \`bind:\`, but the value is owned by ${owner}. Consider creating a binding between ${owner} and ${parent}\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed property \`${prop}\` to ${child} with \`bind:\`, but its parent component ${owner} did not declare \`${prop}\` as a binding. Consider creating a binding between ${owner} and ${parent} (e.g. \`bind:${prop}={...}\` instead of \`${prop}={...}\`)\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_binding`);
}
}
/**
- * %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
- * @param {string | undefined | null} [component]
- * @param {string | undefined | null} [owner]
+ * Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
+ * @param {string} name
+ * @param {string} location
+ * @param {string} prop
+ * @param {string} parent
*/
-export function ownership_invalid_mutation(component, owner) {
+export function ownership_invalid_mutation(name, location, prop, parent) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_mutation\n%c${component ? `${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead` : 'Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'}\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_mutation\n%cMutating unbound props (\`${name}\`, at ${location}) is strongly discouraged. Consider using \`bind:${prop}={...}\` in ${parent} (or using a callback) instead\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_mutation`);
}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 6098b496c5..bf36a595d8 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -49,6 +49,7 @@ export function copy_payload({ out, css, head, uid }) {
*/
export function assign_payload(p1, p2) {
p1.out = p2.out;
+ p1.css = p2.css;
p1.head = p2.head;
p1.uid = p2.uid;
}
diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js
index f9d52cb065..5e7f3152d8 100644
--- a/packages/svelte/src/internal/shared/utils.js
+++ b/packages/svelte/src/internal/shared/utils.js
@@ -10,6 +10,7 @@ export var get_descriptors = Object.getOwnPropertyDescriptors;
export var object_prototype = Object.prototype;
export var array_prototype = Array.prototype;
export var get_prototype_of = Object.getPrototypeOf;
+export var is_extensible = Object.isExtensible;
/**
* @param {any} thing
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index ae6806ec76..2f0a1a831a 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -6,6 +6,12 @@ import {
} from '../internal/client/reactivity/effects.js';
import { get, writable } from './shared/index.js';
import { createSubscriber } from '../reactivity/create-subscriber.js';
+import {
+ active_effect,
+ active_reaction,
+ set_active_effect,
+ set_active_reaction
+} from '../internal/client/runtime.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js';
* @returns {Writable | Readable}
*/
export function toStore(get, set) {
- let init_value = get();
+ var effect = active_effect;
+ var reaction = active_reaction;
+ var init_value = get();
+
const store = writable(init_value, (set) => {
// If the value has changed before we call subscribe, then
// we need to treat the value as already having run
- let ran = init_value !== get();
+ var ran = init_value !== get();
// TODO do we need a different implementation on the server?
- const teardown = effect_root(() => {
- render_effect(() => {
- const value = get();
- if (ran) set(value);
+ var teardown;
+ // Apply the reaction and effect at the time of toStore being called
+ var previous_reaction = active_reaction;
+ var previous_effect = active_effect;
+ set_active_reaction(reaction);
+ set_active_effect(effect);
+
+ try {
+ teardown = effect_root(() => {
+ render_effect(() => {
+ const value = get();
+ if (ran) set(value);
+ });
});
- });
+ } finally {
+ set_active_reaction(previous_reaction);
+ set_active_effect(previous_effect);
+ }
ran = true;
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index d4d106d56d..ada318e85a 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -465,8 +465,10 @@ export function is_raw_text_element(name) {
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
- * @param {string | undefined} location
+ * @template {string | undefined} T
+ * @param {T} location
+ * @returns {T};
*/
export function sanitize_location(location) {
- return location?.replace(/\//g, '/\u200b');
+ return /** @type {T} */ (location?.replace(/\//g, '/\u200b'));
}
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 01e1b390a3..13a69857a7 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.22.6';
+export const VERSION = '5.25.9';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
deleted file mode 100644
index 3bf836f6c5..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
deleted file mode 100644
index 87b88d79cc..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_binding',
- message: 'Cannot bind to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
deleted file mode 100644
index 6c198dc068..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
deleted file mode 100644
index d44806757e..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
deleted file mode 100644
index e4ee2e8635..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
deleted file mode 100644
index d266c95bb8..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
new file mode 100644
index 0000000000..af226559d1
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'rune_invalid_arguments_length',
+ message: '`$state.raw` must be called with zero or one arguments'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
new file mode 100644
index 0000000000..2b50b43b9a
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
new file mode 100644
index 0000000000..442aaad142
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
@@ -0,0 +1 @@
+const foo = $state.raw(1, 2, 3);
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
index 97e470d1c3..7644865495 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
@@ -5,32 +5,20 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
- start: { character: 137, column: 1, line: 11 },
- end: { character: 144, column: 8, line: 11 }
+ start: { character: 191, column: 1, line: 13 },
+ end: { character: 198, column: 8, line: 13 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .f"',
- start: { character: 162, column: 1, line: 12 },
- end: { character: 169, column: 8, line: 12 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".f ~ .g"',
- start: { character: 187, column: 1, line: 13 },
- end: { character: 194, column: 8, line: 13 }
+ start: { character: 216, column: 1, line: 14 },
+ end: { character: 223, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .f"',
- start: { character: 212, column: 1, line: 14 },
- end: { character: 219, column: 8, line: 14 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".b ~ .g"',
- start: { character: 237, column: 1, line: 15 },
- end: { character: 244, column: 8, line: 15 }
+ start: { character: 241, column: 1, line: 15 },
+ end: { character: 248, column: 8, line: 15 }
}
]
});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
index 67a19d10c9..53fca3ae9e 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
@@ -2,10 +2,10 @@
.d.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
+ .f.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
+ .b.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .f { color: red; }*/
- /* (unused) .f ~ .g { color: red; }*/
/* (unused) .b ~ .f { color: red; }*/
- /* (unused) .b ~ .g { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
index 2e2846fa87..52264d3a5a 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
@@ -6,13 +6,13 @@
.d ~ .e { color: green; }
.a ~ .g { color: green; }
.a ~ .b { color: green; }
+ .f ~ .g { color: green; }
+ .b ~ .g { color: green; }
/* no match */
.b ~ .c { color: red; }
.c ~ .f { color: red; }
- .f ~ .g { color: red; }
.b ~ .f { color: red; }
- .b ~ .g { color: red; }
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/expected.css b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
index dcb8a0e481..1863c57d85 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/expected.css
+++ b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
@@ -1,5 +1,10 @@
div.svelte-xyz {
&.class{
- color: red;
+ color: green;
+ }
+ }
+ * {
+ &:hover .class.svelte-xyz {
+ color: green;
}
}
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
index 0c73ed7a78..2c1d2b5ebd 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
+++ b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
@@ -1,7 +1,12 @@
diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js
index 8d89d98cbd..5700a09b96 100644
--- a/packages/svelte/tests/css/samples/has/_config.js
+++ b/packages/svelte/tests/css/samples/has/_config.js
@@ -6,210 +6,238 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"',
start: {
- line: 33,
+ line: 41,
column: 1,
- character: 330
+ character: 378
},
end: {
- line: 33,
+ line: 41,
column: 15,
- character: 344
+ character: 392
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"',
start: {
- line: 36,
+ line: 44,
column: 1,
- character: 365
+ character: 413
},
end: {
- line: 36,
+ line: 44,
column: 24,
- character: 388
+ character: 436
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"',
start: {
- line: 39,
+ line: 47,
column: 1,
- character: 409
+ character: 457
},
end: {
- line: 39,
+ line: 47,
column: 15,
- character: 423
+ character: 471
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 42,
+ line: 50,
column: 1,
- character: 444
+ character: 492
},
end: {
- line: 42,
+ line: 50,
column: 27,
- character: 470
+ character: 518
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: {
- line: 52,
+ line: 60,
column: 1,
- character: 578
+ character: 626
},
end: {
- line: 52,
+ line: 60,
column: 22,
- character: 599
+ character: 647
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
- line: 71,
+ line: 79,
column: 2,
- character: 804
+ character: 852
},
end: {
- line: 71,
+ line: 79,
column: 9,
- character: 811
+ character: 859
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"',
start: {
- line: 87,
+ line: 95,
column: 1,
- character: 958
+ character: 1006
},
end: {
- line: 87,
+ line: 95,
column: 17,
- character: 974
+ character: 1022
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"',
start: {
- line: 90,
+ line: 98,
column: 1,
- character: 995
+ character: 1043
},
end: {
- line: 90,
+ line: 98,
column: 21,
- character: 1015
+ character: 1063
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"',
start: {
- line: 100,
+ line: 108,
column: 1,
- character: 1115
+ character: 1163
},
end: {
- line: 100,
+ line: 108,
column: 11,
- character: 1125
+ character: 1173
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"',
start: {
- line: 103,
+ line: 111,
column: 1,
- character: 1146
+ character: 1194
},
end: {
- line: 103,
+ line: 111,
column: 11,
- character: 1156
+ character: 1204
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
- line: 123,
+ line: 131,
column: 1,
- character: 1348
+ character: 1396
},
end: {
- line: 123,
+ line: 131,
column: 11,
- character: 1358
+ character: 1406
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "d:has(+ f)"',
+ start: {
+ line: 141,
+ column: 1,
+ character: 1494
+ },
+ end: {
+ line: 141,
+ column: 11,
+ character: 1504
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "f:has(~ d)"',
start: {
- line: 133,
+ line: 144,
column: 1,
- character: 1446
+ character: 1525
},
end: {
- line: 133,
+ line: 144,
column: 11,
- character: 1456
+ character: 1535
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
- line: 141,
+ line: 152,
column: 2,
- character: 1529
+ character: 1608
},
end: {
- line: 141,
+ line: 152,
column: 15,
- character: 1542
+ character: 1621
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
- line: 147,
+ line: 158,
column: 2,
- character: 1600
+ character: 1679
},
end: {
- line: 147,
+ line: 158,
column: 16,
- character: 1614
+ character: 1693
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 155,
+ line: 166,
column: 1,
- character: 1684
+ character: 1763
},
end: {
- line: 155,
+ line: 166,
column: 27,
- character: 1710
+ character: 1789
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "h:has(> h > i)"',
+ start: {
+ line: 173,
+ column: 1,
+ character: 1848
+ },
+ end: {
+ line: 173,
+ column: 15,
+ character: 1862
}
}
]
diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css
index b257370d61..2ce4d2bec5 100644
--- a/packages/svelte/tests/css/samples/has/expected.css
+++ b/packages/svelte/tests/css/samples/has/expected.css
@@ -118,6 +118,9 @@
d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
color: green;
}
+ /* (unused) d:has(+ f) {
+ color: red;
+ }*/
/* (unused) f:has(~ d) {
color: red;
}*/
@@ -143,3 +146,13 @@
/* (unused) :global(.foo):has(.unused) {
color: red;
}*/
+
+ g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) {
+ color: green;
+ }
+ /* (unused) h:has(> h > i) {
+ color: red;
+ }*/
+ g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) {
+ color: green;
+ }
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte
index 9b254996bf..033471bc16 100644
--- a/packages/svelte/tests/css/samples/has/input.svelte
+++ b/packages/svelte/tests/css/samples/has/input.svelte
@@ -9,6 +9,14 @@
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
index f623b92cc3..292c6c49ac 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js
+++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
@@ -1,20 +1,5 @@
import { test } from '../../test';
export default test({
- warnings: [
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector "div + div"',
- start: {
- line: 19,
- column: 1,
- character: 185
- },
- end: {
- line: 19,
- column: 10,
- character: 194
- }
- }
- ]
+ warnings: []
});
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
index 9ced15e964..3e449286c9 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css
+++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
@@ -2,9 +2,12 @@
div.svelte-xyz div:where(.svelte-xyz) {
color: green;
}
- /* (unused) div + div {
- color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/
- }*/
+ div.svelte-xyz + div:where(.svelte-xyz) {
+ color: green;
+ }
div.svelte-xyz:has(div:where(.svelte-xyz)) {
color: green;
}
+ span.svelte-xyz:has(~span:where(.svelte-xyz)) {
+ color: green;
+ }
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
index ade8df5744..3c55261f18 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
+++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
@@ -12,14 +12,22 @@
{/snippet}
+{#snippet c()}
+
+ {@render c()}
+{/snippet}
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
new file mode 100644
index 0000000000..1df9f35e50
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{@render foo()}
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
new file mode 100644
index 0000000000..837fa20ae1
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
@@ -0,0 +1,20 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "n + m"',
+ end: {
+ character: 468,
+ column: 6,
+ line: 36
+ },
+ start: {
+ character: 463,
+ column: 1,
+ line: 36
+ }
+ }
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
new file mode 100644
index 0000000000..d2657ccd21
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
@@ -0,0 +1,8 @@
+ x.svelte-xyz + y:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + v:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ y.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ v.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ .component + z.svelte-xyz { color: green; }
+
+ /* (unused) n + m { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
new file mode 100644
index 0000000000..8d80acffb3
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
@@ -0,0 +1,37 @@
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
index c68fb40dea..20639600d0 100644
--- a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
+++ b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
@@ -1,3 +1,4 @@
-
Foo
\ No newline at end of file
+
Foo
+
Bar
diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css
index afc84d52eb..e216a4d3ad 100644
--- a/packages/svelte/tests/css/samples/view-transition/expected.css
+++ b/packages/svelte/tests/css/samples/view-transition/expected.css
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte
index ebb2b3fd88..345213ccd3 100644
--- a/packages/svelte/tests/css/samples/view-transition/input.svelte
+++ b/packages/svelte/tests/css/samples/view-transition/input.svelte
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js
index 0ebf1fa6bd..4c9e2a7253 100644
--- a/packages/svelte/tests/html_equal.js
+++ b/packages/svelte/tests/html_equal.js
@@ -86,7 +86,7 @@ export function normalize_html(
clean_children(node);
return node.innerHTML;
} catch (err) {
- throw new Error(`Failed to normalize HTML:\n${html}`);
+ throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`);
}
}
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
index f2efb1db80..f138c3a070 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
@@ -21,6 +21,9 @@
*/
export let type_no_comment;
+ /** @type {boolean} type_with_comment - One-line declaration with comment */
+ export let type_with_comment;
+
/**
* This is optional
*/
@@ -40,4 +43,10 @@
export let inline_multiline_trailing_comment = 'world'; /*
* this is a same-line trailing multiline comment
**/
+
+ /** @type {number} [default_value=1] */
+ export let default_value = 1;
+
+ /** @type {number} [comment_default_value=1] - This has a comment and an optional value. */
+ export let comment_default_value = 1;
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
index 19fbe38b50..32133ccd4c 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
@@ -9,12 +9,18 @@
+
+
+
+
+
+
/**
* @typedef {Object} Props
* @property {string} comment - My wonderful comment
@@ -22,11 +28,14 @@
* @property {any} one_line - one line comment
* @property {any} no_comment
* @property {boolean} type_no_comment
+ * @property {boolean} type_with_comment - One-line declaration with comment
* @property {any} [optional] - This is optional
* @property {any} inline_commented - this should stay a comment
* @property {any} inline_commented_merged - This comment should be merged - with this inline comment
* @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment
* @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment
+ * @property {number} [default_value]
+ * @property {number} [comment_default_value] - This has a comment and an optional value.
*/
/** @type {Props} */
@@ -36,10 +45,13 @@
one_line,
no_comment,
type_no_comment,
+ type_with_comment,
optional = {stuff: true},
inline_commented,
inline_commented_merged,
inline_multiline_leading_comment = 'world',
- inline_multiline_trailing_comment = 'world'
+ inline_multiline_trailing_comment = 'world',
+ default_value = 1,
+ comment_default_value = 1
} = $props();
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
new file mode 100644
index 0000000000..024f719fb9
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
new file mode 100644
index 0000000000..0903299d95
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
new file mode 100644
index 0000000000..73347c4d7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
@@ -0,0 +1,11 @@
+
+
+{my_prop.foo}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
new file mode 100644
index 0000000000..81005cf737
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
@@ -0,0 +1,14 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ assert.deepEqual(logs, ['bar']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
new file mode 100644
index 0000000000..f38b37fb7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+{#if value !== undefined}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
new file mode 100644
index 0000000000..5bfb777128
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
new file mode 100644
index 0000000000..0eb68310cb
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ target }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ btn1.click();
+ flushSync();
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
new file mode 100644
index 0000000000..9c72d2c48a
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
@@ -0,0 +1,16 @@
+
+
+{#if state}
+ {@const attributes = { title: state.title }}
+
+{/if}
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
new file mode 100644
index 0000000000..761f303c2e
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
@@ -0,0 +1,12 @@
+
+
+
{count}
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
new file mode 100644
index 0000000000..2ffb7e653f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
@@ -0,0 +1,68 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1, btn2, btn3] = target.querySelectorAll('button');
+ let ps = [...target.querySelectorAll('p')];
+
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ // prop update normally if we are not unmounting
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '1');
+ }
+
+ flushSync(() => {
+ btn3.click();
+ });
+
+ // binding still works and update the value correctly
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ console.warn(logs);
+
+ // the five components guarded by `count < 2` unmount and log
+ assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
+
+ flushSync(() => {
+ btn2.click();
+ });
+
+ // the three components guarded by `show` unmount and log
+ assert.deepEqual(logs, [
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 2,
+ true,
+ 2,
+ true,
+ 2,
+ true
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
new file mode 100644
index 0000000000..73a7501e9d
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
new file mode 100644
index 0000000000..b364a989f4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `
`
);
const input = target.querySelector('input');
@@ -30,7 +30,7 @@ export default test({
flushSync(() => input.dispatchEvent(new Event('change', { bubbles: true })));
assert.deepEqual(warnings, [
- 'Assignment to `items` property (main.svelte:8:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
+ 'Assignment to `items` property (main.svelte:9:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
index ad94c4e56e..a79fe873b7 100644
--- a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
@@ -3,6 +3,7 @@
let entries = $state([]);
let object = $state({ items: null, group: [] });
+ let elementFunBind = $state();