diff --git a/.changeset/nice-pianos-punch.md b/.changeset/nice-pianos-punch.md
deleted file mode 100644
index f70af9eb04..0000000000
--- a/.changeset/nice-pianos-punch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: prevent state runes from being called with spread
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
index 90d219faae..b1ba217e5a 100644
--- a/.github/workflows/pkg.pr.new.yml
+++ b/.github/workflows/pkg.pr.new.yml
@@ -8,16 +8,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: install corepack
- run: npm i -g corepack@0.31.0
-
- - run: corepack enable
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
cache: pnpm
- name: Install dependencies
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/01-introduction/04-svelte-js-files.md b/documentation/docs/01-introduction/04-svelte-js-files.md
index 0e05484299..1d3e3dd61a 100644
--- a/documentation/docs/01-introduction/04-svelte-js-files.md
+++ b/documentation/docs/01-introduction/04-svelte-js-files.md
@@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files
Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files.
-These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app.
+These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app (though note that you [cannot export reassigned state]($state#Passing-state-across-modules)).
> [!LEGACY]
> This is a concept that didn't exist prior to Svelte 5
diff --git a/documentation/docs/01-introduction/xx-props.md b/documentation/docs/01-introduction/xx-props.md
deleted file mode 100644
index cad854d878..0000000000
--- a/documentation/docs/01-introduction/xx-props.md
+++ /dev/null
@@ -1,139 +0,0 @@
----
-title: Public API of a component
----
-
-### Public API of a component
-
-Svelte uses the `$props` rune to declare _properties_ or _props_, which means describing the public interface of the component which becomes accessible to consumers of the component.
-
-> [!NOTE] `$props` is one of several runes, which are special hints for Svelte's compiler to make things reactive.
-
-```svelte
-
-```
-
-You can specify a fallback value for a prop. It will be used if the component's consumer doesn't specify the prop on the component when instantiating the component, or if the passed value is `undefined` at some point.
-
-```svelte
-
-```
-
-To get all properties, use rest syntax:
-
-```svelte
-
-```
-
-You can use reserved words as prop names.
-
-```svelte
-
-```
-
-If you're using TypeScript, you can declare the prop types:
-
-```svelte
-
-```
-
-If you're using JavaScript, you can declare the prop types using JSDoc:
-
-```svelte
-
-```
-
-If you export a `const`, `class` or `function`, it is readonly from outside the component.
-
-```svelte
-
-```
-
-Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](bindings#bind:this).
-
-### Reactive variables
-
-To change component state and trigger a re-render, just assign to a locally declared variable that was declared using the `$state` rune.
-
-Update expressions (`count += 1`) and property assignments (`obj.x = y`) have the same effect.
-
-```svelte
-
-```
-
-Svelte's `
-```
-
-If you'd like to react to changes to a prop, use the `$derived` or `$effect` runes instead.
-
-```svelte
-
-```
-
-For more information on reactivity, read the documentation around runes.
diff --git a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md b/documentation/docs/01-introduction/xx-reactivity-fundamentals.md
deleted file mode 100644
index d5e67ada71..0000000000
--- a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md
+++ /dev/null
@@ -1,144 +0,0 @@
----
-title: Reactivity fundamentals
----
-
-Reactivity is at the heart of interactive UIs. When you click a button, you expect some kind of response. It's your job as a developer to make this happen. It's Svelte's job to make your job as intuitive as possible, by providing a good API to express reactive systems.
-
-## Runes
-
-Svelte 5 uses _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and inside `.svelte.js` and `.svelte.ts` modules.
-
-Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.
-
-The following sections introduce the most important runes for declare state, derived state and side effects at a high level. For more details refer to the later sections on [state](state) and [side effects](side-effects).
-
-## `$state`
-
-Reactive state is declared with the `$state` rune:
-
-```svelte
-
-
-
-```
-
-You can also use `$state` in class fields (whether public or private):
-
-```js
-// @errors: 7006 2554
-class Todo {
- done = $state(false);
- text = $state();
-
- constructor(text) {
- this.text = text;
- }
-}
-```
-
-> [!LEGACY]
-> In Svelte 4, state was implicitly reactive if the variable was declared at the top level
->
-> ```svelte
->
->
->
-> ```
-
-## `$derived`
-
-Derived state is declared with the `$derived` rune:
-
-```svelte
-
-
-
-
-
{count} doubled is {doubled}
-```
-
-The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions.
-
-As with `$state`, you can mark class fields as `$derived`.
-
-> [!LEGACY]
-> In Svelte 4, you could use reactive statements for this.
->
-> ```svelte
->
->
->
->
->
{count} doubled is {doubled}
-> ```
->
-> This only worked at the top level of a component.
-
-## `$effect`
-
-To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)):
-
-```svelte
-
-
-
-```
-
-The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
-
-> [!LEGACY]
-> In Svelte 4, you could use reactive statements for this.
->
-> ```svelte
->
->
->
-> ```
->
-> This only worked at the top level of a component.
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index 1b094d375b..26541072f3 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -279,3 +279,83 @@ console.log(total.value); // 7
```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead.
+
+## Passing state across modules
+
+You can declare state in `.svelte.js` and `.svelte.ts` files, but you can only _export_ that state if it's not directly reassigned. In other words you can't do this:
+
+```js
+/// file: state.svelte.js
+export let count = $state(0);
+
+export function increment() {
+ count += 1;
+}
+```
+
+That's because every reference to `count` is transformed by the Svelte compiler — the code above is roughly equivalent to this:
+
+```js
+/// file: state.svelte.js (compiler output)
+// @filename: index.ts
+interface Signal {
+ value: T;
+}
+
+interface Svelte {
+ state(value?: T): Signal;
+ get(source: Signal): T;
+ set(source: Signal, value: T): void;
+}
+declare const $: Svelte;
+// ---cut---
+export let count = $.state(0);
+
+export function increment() {
+ $.set(count, $.get(count) + 1);
+}
+```
+
+> [!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the [playground](/playground).
+
+Since the compiler only operates on one file at a time, if another file imports `count` Svelte doesn't know that it needs to wrap each reference in `$.get` and `$.set`:
+
+```js
+// @filename: state.svelte.js
+export let count = 0;
+
+// @filename: index.js
+// ---cut---
+import { count } from './state.svelte.js';
+
+console.log(typeof count); // 'object', not 'number'
+```
+
+This leaves you with two options for sharing state between modules — either don't reassign it...
+
+```js
+// This is allowed — since we're updating
+// `counter.count` rather than `counter`,
+// Svelte doesn't wrap it in `$.state`
+export const counter = $state({
+ count: 0
+});
+
+export function increment() {
+ counter.count += 1;
+}
+```
+
+...or don't directly export it:
+
+```js
+let count = $state(0);
+
+export function getCount() {
+ return count;
+}
+
+export function increment() {
+ count += 1;
+}
+```
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index ae1a2146c9..46ea9b81e9 100644
--- a/documentation/docs/02-runes/04-$effect.md
+++ b/documentation/docs/02-runes/04-$effect.md
@@ -25,7 +25,7 @@ You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4
});
-
+
```
When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
@@ -135,19 +135,33 @@ An effect only reruns when the object it reads changes, not when a property insi
An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
-For instance, if `a` is `true` in the code snippet below, the code inside the `if` block will run and `b` will be evaluated. As such, changes to either `a` or `b` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE3VQzWrDMAx-FdUU4kBp71li6EPstOxge0ox8-QQK2PD-N1nLy2F0Z2Evj9_chKkP1B04pnYscc3cRCT8xhF95IEf8-Vq0DBr8rzPB_jJ3qumNERH-E2ECNxiRF9tIubWY00lgcYNAywj6wZJS8rtk83wjwgCrXHaULLUrYwKEgVGrnkx-Dx6MNFNstK5OjSbFGbwE0gdXuT_zGYrjmAuco515Hr1p_uXak3K3MgCGS9s-9D2grU-judlQYXIencnzad-tdR79qZrMyvw9wd5Z8Yv1h09dz8mn8AkM7Pfo0BAAA=).
+For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
-Conversely, if `a` is `false`, `b` will not be evaluated, and the effect will _only_ re-run when `a` changes.
+Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.
```ts
-let a = false;
-let b = false;
+// @filename: ambient.d.ts
+declare module 'canvas-confetti' {
+ interface ConfettiOptions {
+ colors: string[];
+ }
+
+ function confetti(opts?: ConfettiOptions): void;
+ export default confetti;
+}
+
+// @filename: index.js
// ---cut---
-$effect(() => {
- console.log('running');
+import confetti from 'canvas-confetti';
- if (a) {
- console.log('b:', b);
+let condition = $state(true);
+let color = $state('#ff3e00');
+
+$effect(() => {
+ if (condition) {
+ confetti({ colors: [color] });
+ } else {
+ confetti();
}
});
```
@@ -211,20 +225,19 @@ It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svel
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase.
-```svelte
-
+// later...
+destroy();
```
## When not to use `$effect`
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/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/13-transition.md
index 51c11e8b34..c51175c272 100644
--- a/documentation/docs/03-template-syntax/13-transition.md
+++ b/documentation/docs/03-template-syntax/13-transition.md
@@ -22,10 +22,6 @@ The `transition:` directive indicates a _bidirectional_ transition, which means
{/if}
```
-## Built-in transitions
-
-A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
-
## Local vs global
Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, _not_ when parent blocks are created or destroyed.
@@ -40,6 +36,10 @@ Transitions are local by default. Local transitions only play when the block the
{/if}
```
+## Built-in transitions
+
+A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
+
## Transition parameters
Transitions can have parameters.
diff --git a/documentation/docs/03-template-syntax/xx-control-flow.md b/documentation/docs/03-template-syntax/xx-control-flow.md
deleted file mode 100644
index b73917997b..0000000000
--- a/documentation/docs/03-template-syntax/xx-control-flow.md
+++ /dev/null
@@ -1,111 +0,0 @@
----
-title: Control flow
----
-
-- if
-- each
-- await (or move that into some kind of data loading section?)
-- NOT: key (move into transition section, because that's the common use case)
-
-Svelte augments HTML with control flow blocks to be able to express conditionally rendered content or lists.
-
-The syntax between these blocks is the same:
-
-- `{#` denotes the start of a block
-- `{:` denotes a different branch part of the block. Depending on the block, there can be multiple of these
-- `{/` denotes the end of a block
-
-## {#if ...}
-
-## {#each ...}
-
-```svelte
-
-{#each expression as name}...{/each}
-```
-
-```svelte
-
-{#each expression as name, index}...{/each}
-```
-
-```svelte
-
-{#each expression as name (key)}...{/each}
-```
-
-```svelte
-
-{#each expression as name, index (key)}...{/each}
-```
-
-```svelte
-
-{#each expression as name}...{:else}...{/each}
-```
-
-Iterating over lists of values can be done with an each block.
-
-```svelte
-
Shopping list
-
- {#each items as item}
-
{item.name} x {item.qty}
- {/each}
-
-```
-
-You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property.
-
-An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback:
-
-```svelte
-{#each items as item, i}
-
{i + 1}: {item.name} x {item.qty}
-{/each}
-```
-
-If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
-
-```svelte
-{#each items as item (item.id)}
-
{item.name} x {item.qty}
-{/each}
-
-
-{#each items as item, i (item.id)}
-
{i + 1}: {item.name} x {item.qty}
-{/each}
-```
-
-You can freely use destructuring and rest patterns in each blocks.
-
-```svelte
-{#each items as { id, name, qty }, i (id)}
-
{i + 1}: {name} x {qty}
-{/each}
-
-{#each objects as { id, ...rest }}
-
{id}
-{/each}
-
-{#each items as [id, ...rest]}
-
{id}
-{/each}
-```
-
-An each block can also have an `{:else}` clause, which is rendered if the list is empty.
-
-```svelte
-{#each todos as todo}
-
{todo.text}
-{:else}
-
No tasks today!
-{/each}
-```
-
-It is possible to iterate over iterables like `Map` or `Set`. Iterables need to be finite and static (they shouldn't change while being iterated over). Under the hood, they are transformed to an array using `Array.from` before being passed off to rendering. If you're writing performance-sensitive code, try to avoid iterables and use regular arrays as they are more performant.
-
-## Other block types
-
-Svelte also provides [`#snippet`](snippets), [`#key`](transitions-and-animations) and [`#await`](data-fetching) blocks. You can find out more about them in their respective sections.
diff --git a/documentation/docs/03-template-syntax/xx-data-fetching.md b/documentation/docs/03-template-syntax/xx-data-fetching.md
deleted file mode 100644
index 4526d51335..0000000000
--- a/documentation/docs/03-template-syntax/xx-data-fetching.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: Data fetching
----
-
-Fetching data is a fundamental part of apps interacting with the outside world. Svelte is unopinionated with how you fetch your data. The simplest way would be using the built-in `fetch` method:
-
-```svelte
-
-```
-
-While this works, it makes working with promises somewhat unergonomic. Svelte alleviates this problem using the `#await` block.
-
-## {#await ...}
-
-## SvelteKit loaders
-
-Fetching inside your components is great for simple use cases, but it's prone to data loading waterfalls and makes code harder to work with because of the promise handling. SvelteKit solves this problem by providing a opinionated data loading story that is coupled to its router. Learn more about it [in the docs](../kit).
diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md
index b698323a04..4204bcfe6d 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -94,7 +94,7 @@ interface User {}
// ---cut---
import { getContext, setContext } from 'svelte';
-let key = {};
+const key = {};
/** @param {User} user */
export function setUserContext(user) {
diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md
index 7e25cdab55..ed5c6277c0 100644
--- a/documentation/docs/07-misc/99-faq.md
+++ b/documentation/docs/07-misc/99-faq.md
@@ -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/07-misc/xx-reactivity-indepth.md b/documentation/docs/07-misc/xx-reactivity-indepth.md
deleted file mode 100644
index b40072552f..0000000000
--- a/documentation/docs/07-misc/xx-reactivity-indepth.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Reactivity in depth
----
-
-- how to think about Runes ("just JavaScript" with added reactivity, what this means for keeping reactivity alive across boundaries)
-- signals
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 901c49822c..32348bb781 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -21,7 +21,7 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
### component_api_changed
```
-%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
+Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
```
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
@@ -151,6 +151,8 @@ This error occurs when state is updated while evaluating a `$derived`. You might
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);
```
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-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md
index 57396bd7fd..0e94cbadb2 100644
--- a/documentation/docs/98-reference/.generated/compile-warnings.md
+++ b/documentation/docs/98-reference/.generated/compile-warnings.md
@@ -823,15 +823,16 @@ See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more
### state_referenced_locally
```
-State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
```
This warning is thrown when the compiler detects the following:
+
- A reactive variable is declared
-- the variable is reassigned
-- the variable is referenced inside the same scope it is declared and it is a non-reactive context
+- ...and later reassigned...
+- ...and referenced in the same scope
-In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
+This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned:
```svelte
diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md
index 0102aafcbc..4c81d7b894 100644
--- a/documentation/docs/98-reference/.generated/shared-errors.md
+++ b/documentation/docs/98-reference/.generated/shared-errors.md
@@ -30,6 +30,12 @@ This error would be thrown in a setup like this:
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
+### invalid_snippet_arguments
+
+```
+A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+```
+
### lifecycle_outside_component
```
diff --git a/package.json b/package.json
index ad69bfc9ca..70e85438f0 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"private": true,
"type": "module",
"license": "MIT",
- "packageManager": "pnpm@9.4.0",
+ "packageManager": "pnpm@10.4.0",
"engines": {
"pnpm": ">=9.0.0"
},
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 86e1179029..8b46efc94c 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,97 @@
# svelte
+## 5.26.1
+
+### Patch Changes
+
+- fix: update `state_referenced_locally` message ([#15733](https://github.com/sveltejs/svelte/pull/15733))
+
+## 5.26.0
+
+### Minor Changes
+
+- feat: add `css.hasGlobal` to `compile` output ([#15450](https://github.com/sveltejs/svelte/pull/15450))
+
+### Patch Changes
+
+- fix: add snippet argument validation in dev ([#15521](https://github.com/sveltejs/svelte/pull/15521))
+
+## 5.25.12
+
+### Patch Changes
+
+- fix: improve internal_set versioning mechanic ([#15724](https://github.com/sveltejs/svelte/pull/15724))
+
+- fix: don't transform reassigned state in labeled statement in `$derived` ([#15725](https://github.com/sveltejs/svelte/pull/15725))
+
+## 5.25.11
+
+### Patch Changes
+
+- fix: handle hydration mismatches in await blocks ([#15708](https://github.com/sveltejs/svelte/pull/15708))
+
+- fix: prevent ownership warnings if the fallback of a bindable is used ([#15720](https://github.com/sveltejs/svelte/pull/15720))
+
+## 5.25.10
+
+### Patch Changes
+
+- fix: set deriveds as `CLEAN` if they are assigned to ([#15592](https://github.com/sveltejs/svelte/pull/15592))
+
+- fix: better scope `:global()` with nesting selector `&` ([#15671](https://github.com/sveltejs/svelte/pull/15671))
+
+## 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
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index 572930843e..c4e68f8fee 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -12,7 +12,7 @@
## component_api_changed
-> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
+> Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
@@ -107,6 +107,8 @@ This error occurs when state is updated while evaluating a `$derived`. You might
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);
```
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-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md
index 8c32fb7082..6603759156 100644
--- a/packages/svelte/messages/compile-warnings/script.md
+++ b/packages/svelte/messages/compile-warnings/script.md
@@ -54,14 +54,15 @@ To fix this, wrap your variable declaration with `$state`.
## state_referenced_locally
-> State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+> This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
This warning is thrown when the compiler detects the following:
+
- A reactive variable is declared
-- the variable is reassigned
-- the variable is referenced inside the same scope it is declared and it is a non-reactive context
+- ...and later reassigned...
+- ...and referenced in the same scope
-In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
+This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned:
```svelte
diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md
index 8b4c61303a..20f3d193d9 100644
--- a/packages/svelte/messages/shared-errors/errors.md
+++ b/packages/svelte/messages/shared-errors/errors.md
@@ -26,6 +26,10 @@ This error would be thrown in a setup like this:
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
+## invalid_snippet_arguments
+
+> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 56fa949391..a06c73429a 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.25.2",
+ "version": "5.26.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@@ -156,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/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 9d79d88b23..523389a25a 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -944,54 +944,53 @@ const instance_script = {
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
- const ids = extract_identifiers(node.body.expression.left);
- const [, expression_ids] = extract_all_identifiers_from_expression(
- node.body.expression.right
- );
- const bindings = ids.map((id) => state.scope.get(id.name));
- const reassigned_bindings = bindings.filter((b) => b?.reassigned);
+ const { left, right } = node.body.expression;
- if (
- node.body.expression.right.type !== 'Literal' &&
- !bindings.some((b) => b?.kind === 'store_sub') &&
- node.body.expression.left.type !== 'MemberExpression'
- ) {
- let { start, end } = /** @type {{ start: number, end: number }} */ (
- node.body.expression.right
- );
+ const ids = extract_identifiers(left);
+ const [, expression_ids] = extract_all_identifiers_from_expression(right);
+ const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name)));
- check_rune_binding('derived');
+ if (bindings.every((b) => b.kind === 'legacy_reactive')) {
+ if (
+ right.type !== 'Literal' &&
+ bindings.every((b) => b.kind !== 'store_sub') &&
+ left.type !== 'MemberExpression'
+ ) {
+ let { start, end } = /** @type {{ start: number, end: number }} */ (right);
- // $derived
- state.str.update(
- /** @type {number} */ (node.start),
- /** @type {number} */ (node.body.expression.start),
- 'let '
- );
+ check_rune_binding('derived');
- if (node.body.expression.right.type === 'SequenceExpression') {
- while (state.str.original[start] !== '(') start -= 1;
- while (state.str.original[end - 1] !== ')') end += 1;
- }
+ // $derived
+ state.str.update(
+ /** @type {number} */ (node.start),
+ /** @type {number} */ (node.body.expression.start),
+ 'let '
+ );
+
+ if (right.type === 'SequenceExpression') {
+ while (state.str.original[start] !== '(') start -= 1;
+ while (state.str.original[end - 1] !== ')') end += 1;
+ }
+
+ state.str.prependRight(start, `$derived(`);
- state.str.prependRight(start, `$derived(`);
+ // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
+ // otherwise, we need to add one
+ if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
+ state.str.appendLeft(end, `)`);
+ }
- // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
- // otherwise, we need to add one
- if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
- state.str.appendLeft(end, `)`);
+ return;
}
- return;
- } else {
- for (const binding of reassigned_bindings) {
- if (binding && (ids.includes(binding.node) || expression_ids.length === 0)) {
+ for (const binding of bindings) {
+ if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
check_rune_binding('state');
const init =
binding.kind === 'state'
? ' = $state()'
: expression_ids.length === 0
- ? ` = $state(${state.str.original.substring(/** @type {number} */ (node.body.expression.right.start), node.body.expression.right.end)})`
+ ? ` = $state(${state.str.original.substring(/** @type {number} */ (right.start), right.end)})`
: '';
// implicitly-declared variable which we need to make explicit
state.str.prependLeft(
@@ -1000,7 +999,8 @@ const instance_script = {
);
}
}
- if (expression_ids.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) {
+
+ if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js
index f8c39f1b1d..56dbe124b7 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -118,6 +118,7 @@ function read_rule(parser) {
metadata: {
parent_rule: null,
has_local_selectors: false,
+ has_global_selectors: false,
is_global_block: false
}
};
@@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) {
children,
metadata: {
rule: null,
+ is_global: false,
used: false
}
};
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/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
index 362ac9dcad..76cb2f56e9 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
@@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
- * @typedef {Visitors<
- * AST.CSS.Node,
- * {
- * keyframes: string[];
- * rule: AST.CSS.Rule | null;
- * }
- * >} CssVisitors
+ * @typedef {{
+ * keyframes: string[];
+ * rule: AST.CSS.Rule | null;
+ * analysis: ComponentAnalysis;
+ * }} CssState
+ */
+
+/**
+ * @typedef {Visitors} CssVisitors
*/
/**
@@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) {
);
}
+/**
+ * @param {AST.SvelteNode[]} path
+ */
+function is_unscoped(path) {
+ return path
+ .filter((node) => node.type === 'Rule')
+ .every((node) => node.metadata.has_global_selectors);
+}
+
/**
*
* @param {Array} path
@@ -42,6 +53,9 @@ const css_visitors = {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude);
+ } else if (node.prelude.startsWith('-global-')) {
+ // we don't check if the block.children.length because the keyframe is still added even if empty
+ context.state.analysis.css.has_global ||= is_unscoped(context.path);
}
}
@@ -99,10 +113,12 @@ const css_visitors = {
node.metadata.rule = context.state.rule;
- node.metadata.used ||= node.children.every(
+ node.metadata.is_global = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
+ node.metadata.used ||= node.metadata.is_global;
+
if (
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
@@ -190,6 +206,7 @@ const css_visitors = {
if (idx !== -1) {
is_global_block = true;
+
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
@@ -242,16 +259,26 @@ const css_visitors = {
}
}
- context.next({
- ...context.state,
- rule: node
- });
+ const state = { ...context.state, rule: node };
- node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
- return selector.children.some(
- ({ metadata }) => !metadata.is_global && !metadata.is_global_like
- );
- });
+ // visit selector list first, to populate child selector metadata
+ context.visit(node.prelude, state);
+
+ for (const selector of node.prelude.children) {
+ node.metadata.has_global_selectors ||= selector.metadata.is_global;
+ node.metadata.has_local_selectors ||= !selector.metadata.is_global;
+ }
+
+ // if this rule has a ComplexSelector whose RelativeSelector children are all
+ // `:global(...)`, and the rule contains declarations (rather than just
+ // nested rules) then the component as a whole includes global CSS
+ context.state.analysis.css.has_global ||=
+ node.metadata.has_global_selectors &&
+ node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
+ is_unscoped(context.path);
+
+ // visit block list, so parent rule metadata is populated
+ context.visit(node.block, state);
},
NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@@ -289,5 +316,12 @@ const css_visitors = {
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
- walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
+ /** @type {CssState} */
+ const css_state = {
+ keyframes: analysis.css.keyframes,
+ rule: null,
+ analysis
+ };
+
+ walk(stylesheet, css_state, css_visitors);
}
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 070ec7cd34..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,6 +1,11 @@
/** @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';
@@ -251,7 +256,11 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
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 (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true;
@@ -282,20 +291,26 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
* 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);
}
}
@@ -304,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;
@@ -814,10 +830,10 @@ function get_element_parent(node) {
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set} seen
- * @returns {Map}
+ * @returns {Map}
*/
function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
- /** @type {Map} */
+ /** @type {Map} */
const result = new Map();
const path = node.metadata.path;
@@ -847,14 +863,18 @@ function get_possible_element_siblings(node, direction, adjacent_only, seen = ne
}
// 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)) {
- if (node.type === 'SlotElement') {
+ } 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_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 === 'SvelteElement') {
@@ -907,7 +927,7 @@ function get_possible_element_siblings(node, direction, adjacent_only, seen = ne
}
/**
- * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock} 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
@@ -942,6 +962,10 @@ function get_possible_nested_siblings(node, direction, adjacent_only, seen = new
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 */
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 1f636c32df..a6eb9565cb 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,
@@ -455,7 +456,8 @@ export function analyze_component(root, source, options) {
hash
})
: '',
- keyframes: []
+ keyframes: [],
+ has_global: false
},
source,
undefined_exports: new Map(),
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
index 79dccd5a7c..dcbe564543 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
+import { get_rune } from '../../scope.js';
/**
* @param {Identifier} node
@@ -111,7 +112,34 @@ export function Identifier(node, context) {
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
- w.state_referenced_locally(node);
+ let type = 'closure';
+
+ let i = context.path.length;
+ while (i--) {
+ const parent = context.path[i];
+
+ if (
+ parent.type === 'ArrowFunctionExpression' ||
+ parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression'
+ ) {
+ break;
+ }
+
+ if (
+ parent.type === 'CallExpression' &&
+ parent.arguments.includes(/** @type {any} */ (context.path[i + 1]))
+ ) {
+ const rune = get_rune(parent, context.state.scope);
+
+ if (rune === '$state' || rune === '$state.raw') {
+ type = 'derived';
+ break;
+ }
+ }
+ }
+
+ w.state_referenced_locally(node, node.name, type);
}
if (
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 605c016786..adc4164f95 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
@@ -393,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 ||
@@ -530,9 +536,6 @@ export function client_component(analysis, options) {
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
-
- body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
- body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));
}
if (!analysis.runes) {
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 28e3fabb19..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
@@ -110,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/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index 150c56e166..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
@@ -7,9 +7,10 @@ import {
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
-import { dev, is_ignored, locate_node } from '../../../../state.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
@@ -20,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);
}
/**
@@ -165,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/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index b67bb8893c..4081ef7bbf 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
@@ -146,30 +146,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' || 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, field.kind === 'state' && b.true))]
- )
- );
- }
-
- 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}')`)]
- )
- );
- }
+ // set foo(value) { this.#foo = value; }
+ const val = b.id('value');
+
+ body.push(
+ b.method(
+ 'set',
+ definition.key,
+ [val],
+ [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
+ )
+ );
}
continue;
}
@@ -178,33 +165,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/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
index 7a0d6981b5..7eb043aa5d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
@@ -1,4 +1,4 @@
-/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
+/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js';
*/
export function SnippetBlock(node, context) {
// TODO hoist where possible
- /** @type {Pattern[]} */
+ /** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
/** @type {BlockStatement} */
@@ -66,7 +66,18 @@ export function SnippetBlock(node, context) {
}
}
}
-
+ if (dev) {
+ declarations.unshift(
+ b.stmt(
+ b.call(
+ '$.validate_snippet_args',
+ .../** @type {Identifier[]} */ (
+ args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left))
+ )
+ )
+ )
+ );
+ }
body = b.block([
...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
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/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/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/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js
index 5b0dcd5588..dff034f8aa 100644
--- a/packages/svelte/src/compiler/phases/3-transform/css/index.js
+++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js
@@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) {
// generateMap takes care of calculating source relative to file
source: options.filename,
file: options.cssOutputFilename || options.filename
- })
+ }),
+ hasGlobal: analysis.css.has_global
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);
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/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
index 8fc82b8905..2aa534d257 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
-import { empty_comment } from './shared/utils.js';
+import { block_close } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@@ -10,10 +10,10 @@ import { empty_comment } from './shared/utils.js';
*/
export function AwaitBlock(node, context) {
context.state.template.push(
- empty_comment,
b.stmt(
b.call(
'$.await',
+ b.id('$$payload'),
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
@@ -21,13 +21,9 @@ export function AwaitBlock(node, context) {
b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
- ),
- b.arrow(
- node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [],
- node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([])
)
)
),
- empty_comment
+ block_close
);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
index eb83917927..cae3e7d79c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
@@ -1,6 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
+import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
@@ -13,7 +14,9 @@ export function SnippetBlock(node, context) {
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
-
+ if (dev) {
+ fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
+ }
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index abe2b115de..f98cbe1415 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;
@@ -73,6 +74,7 @@ export interface ComponentAnalysis extends Analysis {
ast: AST.CSS.StyleSheet | null;
hash: string;
keyframes: string[];
+ has_global: boolean;
};
source: string;
undefined_exports: Map;
diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts
index 7b2e6ae5f7..154a06ffb1 100644
--- a/packages/svelte/src/compiler/types/css.d.ts
+++ b/packages/svelte/src/compiler/types/css.d.ts
@@ -34,6 +34,10 @@ export namespace _CSS {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
+ /**
+ * `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like
+ */
+ has_global_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
@@ -64,6 +68,7 @@ export namespace _CSS {
/** @internal */
metadata: {
rule: null | Rule;
+ is_global: boolean;
/** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */
used: boolean;
};
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index eec41bad9d..616c346ad3 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -18,6 +18,8 @@ export interface CompileResult {
code: string;
/** A source map */
map: SourceMap;
+ /** Whether or not the CSS includes global rules */
+ hasGlobal: boolean;
};
/**
* An array of warning objects that were generated during compilation. Each warning has several properties:
diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js
index a9ea617d3f..e6fc8caba5 100644
--- a/packages/svelte/src/compiler/warnings.js
+++ b/packages/svelte/src/compiler/warnings.js
@@ -641,11 +641,13 @@ export function reactive_declaration_module_script_dependency(node) {
}
/**
- * State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+ * This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
* @param {null | NodeLike} node
+ * @param {string} name
+ * @param {string} type
*/
-export function state_referenced_locally(node) {
- w(node, 'state_referenced_locally', `State referenced in its own scope will never update. Did you mean to reference it inside a closure?\nhttps://svelte.dev/e/state_referenced_locally`);
+export function state_referenced_locally(node, name, type) {
+ w(node, 'state_referenced_locally', `This reference only captures the initial value of \`${name}\`. Did you mean to reference it inside a ${type} instead?\nhttps://svelte.dev/e/state_referenced_locally`);
}
/**
diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js
index 8861e440fc..6ea407d448 100644
--- a/packages/svelte/src/constants.js
+++ b/packages/svelte/src/constants.js
@@ -22,6 +22,7 @@ export const HYDRATION_START = '[';
/** used to indicate that an `{:else}...` block was rendered */
export const HYDRATION_START_ELSE = '[!';
export const HYDRATION_END = ']';
+export const HYDRATION_AWAIT_THEN = '!';
export const HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1;
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index c6f0e80ab9..4ad912bb81 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -24,6 +24,5 @@ export const EFFECT_IS_UPDATING = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
export const PROXY_ONCHANGE_SYMBOL = Symbol('proxy onchange');
-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 bfca9d5e6a..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 {
@@ -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;
}
diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js
index 138213c551..02428dc824 100644
--- a/packages/svelte/src/internal/client/dev/legacy.js
+++ b/packages/svelte/src/internal/client/dev/legacy.js
@@ -1,7 +1,6 @@
import * as e from '../errors.js';
import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
-import { get_component } from './ownership.js';
/** @param {Function & { [FILENAME]: string }} target */
export function check_target(target) {
@@ -15,9 +14,7 @@ export function legacy_api() {
/** @param {string} method */
function error(method) {
- // @ts-expect-error
- const parent = get_component()?.[FILENAME] ?? 'Something';
- e.component_api_changed(parent, method, component[FILENAME]);
+ e.component_api_changed(method, component[FILENAME]);
}
return {
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index 62119b36db..108c7adf92 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -1,304 +1,80 @@
-/** @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';
-
-/** @type {Record>} */
-const boundaries = {};
-
-const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
-const firefox_pattern = /@(.+):(\d+):(\d+)$/;
-
-function get_stack() {
- const stack = new Error().stack;
- if (!stack) return null;
-
- const entries = [];
-
- for (const line of stack.split('\n')) {
- let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
-
- if (match) {
- entries.push({
- file: match[1],
- line: +match[2],
- column: +match[3]
- });
- }
- }
-
- return entries;
-}
+import { sanitize_location } from '../../../utils.js';
/**
- * Determines which `.svelte` component is responsible for a given state change
- * @returns {Function | null}
+ * 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 get_component() {
- // first 4 lines are svelte internals; adjust this number if we change the internal call stack
- const stack = get_stack()?.slice(4);
- if (!stack) return null;
-
- for (let i = 0; i < stack.length; i++) {
- const entry = stack[i];
- const modules = boundaries[entry.file];
- if (!modules) {
- // If the first entry is not a component, that means the modification very likely happened
- // within a .svelte.js file, possibly triggered by a component. Since these files are not part
- // of the bondaries/component context heuristic, we need to bail in this case, else we would
- // have false positives when the .svelte.ts file provides a state creator function, encapsulating
- // the state and its mutations, and is being called from a component other than the one who
- // called the state creator function.
- if (i === 0) return null;
- continue;
- }
-
- for (const module of modules) {
- if (module.end == null) {
- return null;
- }
- if (module.start.line < entry.line && module.end.line > entry.line) {
- return module.component;
+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_or_unset(props, name) || !parent) {
+ return result;
}
- }
- }
-
- 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
- * for a given state change
- */
-export function mark_module_start() {
- const start = get_stack()?.[2];
-
- if (start) {
- (boundaries[start.file] ??= []).push({
- start,
- // @ts-expect-error
- end: null,
- // @ts-expect-error we add the component at the end, since HMR will overwrite the function
- component: null
- });
- }
-}
+ let value = props[name];
-/**
- * @param {Function} component
- */
-export function mark_module_end(component) {
- const end = get_stack()?.[2];
-
- if (end) {
- const boundaries_file = boundaries[end.file];
- const boundary = boundaries_file[boundaries_file.length - 1];
-
- boundary.end = end;
- boundary.component = component;
- }
-}
-
-/**
- * @param {any} object
- * @param {any | null} owner
- * @param {boolean} [global]
- * @param {boolean} [skip_warning]
- */
-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]);
- }
- }
- }
-
- 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]);
-
- 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_or_unset(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) {
+function is_bound_or_unset(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 (
- metadata?.owners?.values().next().value ??
- get_owner(/** @type {ProxyMetadata} */ (metadata.parent))
+ !!get_descriptor(props, prop_name)?.set ||
+ (is_entry_props && prop_name in props) ||
+ !(prop_name in props)
);
}
-
-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();
- }
- }
-}
diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js
new file mode 100644
index 0000000000..e41e4c4628
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/validation.js
@@ -0,0 +1,15 @@
+import { invalid_snippet_arguments } from '../../shared/errors.js';
+/**
+ * @param {Node} anchor
+ * @param {...(()=>any)[]} args
+ */
+export function validate_snippet_args(anchor, ...args) {
+ if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
+ invalid_snippet_arguments();
+ }
+ for (let arg of args) {
+ if (typeof arg !== 'function') {
+ invalid_snippet_arguments();
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 2e3d229779..99bdc0000c 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -4,9 +4,16 @@ import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
-import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
+import {
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ remove_nodes,
+ set_hydrate_node,
+ set_hydrating
+} from '../hydration.js';
import { queue_micro_task } from '../task.js';
-import { UNINITIALIZED } from '../../../../constants.js';
+import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
is_runes,
@@ -113,6 +120,19 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var effect = block(() => {
if (input === (input = get_input())) return;
+ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
+ // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
+ let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
+
+ if (mismatch) {
+ // Hydration mismatch: remove everything inside the anchor and start fresh
+ anchor = remove_nodes();
+
+ set_hydrate_node(anchor);
+ set_hydrating(false);
+ mismatch = true;
+ }
+
if (is_promise(input)) {
var promise = input;
@@ -155,6 +175,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
update(THEN, false);
}
+ if (mismatch) {
+ // continue in hydration mode
+ set_hydrating(true);
+ }
+
// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
});
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/errors.js b/packages/svelte/src/internal/client/errors.js
index 8a5b5033a7..429dd99da9 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -54,15 +54,14 @@ export function bind_not_bindable(key, component, name) {
}
/**
- * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
- * @param {string} parent
+ * Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
* @param {string} method
* @param {string} component
* @returns {never}
*/
-export function component_api_changed(parent, method, component) {
+export function component_api_changed(method, component) {
if (DEV) {
- const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
+ const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
throw error;
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index a1e7c44370..83ef8824b4 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -4,18 +4,11 @@ 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 { 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';
+export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 6efbfe2cbd..fe1f358501 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,9 +1,8 @@
-/** @import { ProxyMetadata, Source, ValueOptions } from '#client' */
+/** @import { Source, ValueOptions } from '#client' */
import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../../constants.js';
import { tracing_mode_flag } from '../flags/index.js';
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
-import { component_context } from './context.js';
import {
array_prototype,
get_descriptor,
@@ -11,8 +10,7 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
-import { PROXY_ONCHANGE_SYMBOL, STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
-import { check_ownership, widen_ownership } from './dev/ownership.js';
+import { PROXY_ONCHANGE_SYMBOL, STATE_SYMBOL } from './constants.js';
import { get_stack } from './dev/tracing.js';
import * as e from './errors.js';
import { batch_onchange, set, source, state } from './reactivity/sources.js';
@@ -28,17 +26,13 @@ function identity(fn) {
return fn;
}
-/** @type {ProxyMetadata | null} */
-var parent_metadata = null;
-
/**
* @template T
* @param {T} value
* @param {() => void} [onchange]
- * @param {Source} [prev] dev mode only
* @returns {T}
*/
-export function proxy(value, onchange, prev) {
+export function proxy(value, onchange) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null) {
return value;
@@ -84,16 +78,7 @@ export function proxy(value, onchange, prev) {
set_active_reaction(reaction);
/** @type {T} */
- var result;
-
- if (DEV) {
- var previous_metadata = parent_metadata;
- parent_metadata = metadata;
- result = fn();
- parent_metadata = previous_metadata;
- } else {
- result = fn();
- }
+ var result = fn();
set_active_reaction(previous_reaction);
return result;
@@ -105,31 +90,6 @@ export function proxy(value, onchange, prev) {
sources.set('length', source(/** @type {any[]} */ (value).length, onchange, stack));
}
- /** @type {ProxyMetadata} */
- var metadata;
-
- if (DEV) {
- metadata = {
- parent: parent_metadata,
- 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_metadata === null
- ? component_context !== null
- ? new Set([component_context.function])
- : null
- : new Set();
- }
- }
-
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
@@ -194,10 +154,6 @@ export function proxy(value, onchange, prev) {
},
get(target, prop, receiver) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return metadata;
- }
-
if (prop === STATE_SYMBOL) {
return value;
}
@@ -237,22 +193,6 @@ export function proxy(value, onchange, prev) {
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;
}
@@ -293,10 +233,6 @@ export function proxy(value, onchange, prev) {
},
has(target, prop) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return true;
- }
-
if (prop === STATE_SYMBOL) {
return true;
}
@@ -381,14 +317,6 @@ export function proxy(value, onchange, prev) {
);
}
})();
- 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);
- }
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index cd7bbba02f..c9a8f7674a 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -67,6 +67,7 @@ export function derived(fn) {
* @param {() => V} fn
* @returns {Derived}
*/
+/*#__NO_SIDE_EFFECTS__*/
export function user_derived(fn) {
const d = derived(fn);
@@ -130,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 bd85b14df0..341d7c768a 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -7,7 +7,7 @@ import {
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../constants.js';
-import { get_descriptor, is_function } from '../../shared/utils.js';
+import { define_property, 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 { get, captured_signals, untrack } from '../runtime.js';
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 62b8e5dc77..cb50cad39c 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -29,14 +29,14 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
- PROXY_ONCHANGE_SYMBOL,
- EFFECT_IS_UPDATING
+ PROXY_ONCHANGE_SYMBOL
} from '../constants.js';
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 { proxy } from '../proxy.js';
import { component_context, is_runes } from '../context.js';
+import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
export const old_values = new Map();
@@ -108,6 +108,7 @@ export function source(v, o, stack) {
* @param {() => void} [o]
* @param {Error | null} [stack]
*/
+/*#__NO_SIDE_EFFECTS__*/
export function state(v, o, stack) {
const s = source(v, o, stack);
@@ -169,11 +170,7 @@ export function set(source, value, should_proxy = false) {
e.state_unsafe_mutation();
}
- let new_value = should_proxy
- ? DEV
- ? proxy(value, source.o, source)
- : proxy(value, source.o)
- : value;
+ let new_value = should_proxy ? proxy(value, source.o) : value;
return internal_set(source, new_value);
}
@@ -203,7 +200,6 @@ export function internal_set(source, value) {
}
source.v = value;
- source.wv = increment_write_version();
if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
@@ -213,6 +209,16 @@ 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);
+ }
+
+ source.wv = increment_write_version();
+
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 a5d26412a4..a7662be617 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -27,7 +27,7 @@ import {
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
-import { destroy_derived_effects, update_derived } from './reactivity/deriveds.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';
@@ -476,7 +476,7 @@ 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) {
@@ -692,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;
@@ -701,7 +702,6 @@ function flush_queued_root_effects() {
if (DEV) {
dev_effect_stack = [];
}
- old_values.clear();
}
}
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 8c47a17e89..b57e3b7356 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -179,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/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js
index 3c5e860790..9e96ae3430 100644
--- a/packages/svelte/src/internal/server/blocks/snippet.js
+++ b/packages/svelte/src/internal/server/blocks/snippet.js
@@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */
-/** @import { Payload } from '#server' */
+/** @import { Payload } from '../payload' */
/** @import { Getters } from '#shared' */
/**
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index ecf4e67429..34849196b7 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -1,10 +1,12 @@
-/** @import { Component, Payload } from '#server' */
+/** @import { Component } from '#server' */
import { FILENAME } from '../../constants.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
+import { invalid_snippet_arguments } from '../shared/errors.js';
+import { Payload } from './payload.js';
/**
* @typedef {{
@@ -98,3 +100,12 @@ export function push_element(payload, tag, line, column) {
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
}
+
+/**
+ * @param {Payload} payload
+ */
+export function validate_snippet_args(payload) {
+ if (typeof payload !== 'object' || !(payload instanceof Payload)) {
+ invalid_snippet_arguments();
+ }
+}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 6098b496c5..d711778a44 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -1,5 +1,5 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */
-/** @import { Component, Payload, RenderOutput } from '#server' */
+/** @import { Component, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
@@ -13,46 +13,17 @@ import {
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
-import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
+import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
+import { Payload } from './payload.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
-/**
- * @param {Payload} to_copy
- * @returns {Payload}
- */
-export function copy_payload({ out, css, head, uid }) {
- return {
- out,
- css: new Set(css),
- head: {
- title: head.title,
- out: head.out,
- css: new Set(head.css),
- uid: head.uid
- },
- uid
- };
-}
-
-/**
- * Assigns second payload to first
- * @param {Payload} p1
- * @param {Payload} p2
- * @returns {void}
- */
-export function assign_payload(p1, p2) {
- p1.out = p2.out;
- p1.head = p2.head;
- p1.uid = p2.uid;
-}
-
/**
* @param {Payload} payload
* @param {string} tag
@@ -86,16 +57,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
-/**
- * Creates an ID generator
- * @param {string} prefix
- * @returns {() => string}
- */
-function props_id_generator(prefix) {
- let uid = 1;
- return () => `${prefix}s${uid++}`;
-}
-
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
@@ -105,14 +66,7 @@ function props_id_generator(prefix) {
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
- const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : '');
- /** @type {Payload} */
- const payload = {
- out: '',
- css: new Set(),
- head: { title: '', out: '', css: new Set(), uid },
- uid
- };
+ const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy;
on_destroy = [];
@@ -473,18 +427,21 @@ export function bind_props(props_parent, props_now) {
/**
* @template V
+ * @param {Payload} payload
* @param {Promise} promise
* @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn
* @returns {void}
*/
-function await_block(promise, pending_fn, then_fn) {
+function await_block(payload, promise, pending_fn, then_fn) {
if (is_promise(promise)) {
+ payload.out += BLOCK_OPEN;
promise.then(null, noop);
if (pending_fn !== null) {
pending_fn();
}
} else if (then_fn !== null) {
+ payload.out += BLOCK_OPEN_ELSE;
then_fn(promise);
}
}
@@ -541,7 +498,9 @@ export { html } from './blocks/html.js';
export { push, pop } from './context.js';
-export { push_element, pop_element } from './dev.js';
+export { push_element, pop_element, validate_snippet_args } from './dev.js';
+
+export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
new file mode 100644
index 0000000000..03bcaf492e
--- /dev/null
+++ b/packages/svelte/src/internal/server/payload.js
@@ -0,0 +1,64 @@
+export class Payload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+
+ head = {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css: new Set(),
+ title: '',
+ out: '',
+ uid: () => ''
+ };
+
+ constructor(id_prefix = '') {
+ this.uid = props_id_generator(id_prefix);
+ this.head.uid = this.uid;
+ }
+}
+
+/**
+ * Used in legacy mode to handle bindings
+ * @param {Payload} to_copy
+ * @returns {Payload}
+ */
+export function copy_payload({ out, css, head, uid }) {
+ const payload = new Payload();
+
+ payload.out = out;
+ payload.css = new Set(css);
+ payload.uid = uid;
+
+ payload.head = {
+ title: head.title,
+ out: head.out,
+ css: new Set(head.css),
+ uid: head.uid
+ };
+
+ return payload;
+}
+
+/**
+ * Assigns second payload to first
+ * @param {Payload} p1
+ * @param {Payload} p2
+ * @returns {void}
+ */
+export function assign_payload(p1, p2) {
+ p1.out = p2.out;
+ p1.css = p2.css;
+ p1.head = p2.head;
+ p1.uid = p2.uid;
+}
+
+/**
+ * Creates an ID generator
+ * @param {string} prefix
+ * @returns {() => string}
+ */
+function props_id_generator(prefix) {
+ let uid = 1;
+ return () => `${prefix}s${uid++}`;
+}
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index 2fffdbbdf0..6b0fc146c4 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -11,19 +11,6 @@ export interface Component {
function?: any;
}
-export interface Payload {
- out: string;
- css: Set<{ hash: string; code: string }>;
- head: {
- title: string;
- out: string;
- uid: () => string;
- css: Set<{ hash: string; code: string }>;
- };
- /** Function that generates a unique ID */
- uid: () => string;
-}
-
export interface RenderOutput {
/** HTML that goes into the `` */
head: string;
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 26d6822cdb..2e89dc1ad1 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -17,6 +17,21 @@ export function invalid_default_snippet() {
}
}
+/**
+ * A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+ * @returns {never}
+ */
+export function invalid_snippet_arguments() {
+ if (DEV) {
+ const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
+ }
+}
+
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
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 e2dc6c2c09..e5cb34ecd5 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.25.2';
+export const VERSION = '5.26.1';
export const PUBLIC_VERSION = '5';
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-keyframes/_config.js b/packages/svelte/tests/css/samples/global-keyframes/_config.js
new file mode 100644
index 0000000000..30953854ad
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-keyframes/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: true
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/_config.js b/packages/svelte/tests/css/samples/global-local-nested/_config.js
new file mode 100644
index 0000000000..5a7796ebac
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: false
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/expected.css b/packages/svelte/tests/css/samples/global-local-nested/expected.css
new file mode 100644
index 0000000000..8eadf2b948
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/expected.css
@@ -0,0 +1,12 @@
+
+ div.svelte-xyz {
+ .whatever {
+ color: green;
+ }
+ }
+
+ .whatever {
+ div.svelte-xyz {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-local-nested/input.svelte b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
new file mode 100644
index 0000000000..60210be753
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
@@ -0,0 +1,15 @@
+
\ 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/test.ts b/packages/svelte/tests/css/test.ts
index dd51f52eab..8846b1d986 100644
--- a/packages/svelte/tests/css/test.ts
+++ b/packages/svelte/tests/css/test.ts
@@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial;
warnings?: Warning[];
props?: Record;
+ hasGlobal?: boolean;
}
/**
@@ -78,6 +79,14 @@ const { test, run } = suite(async (config, cwd) => {
// assert_html_equal(actual_ssr, expected.html);
}
+ if (config.hasGlobal !== undefined) {
+ const metadata = JSON.parse(
+ fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
+ );
+
+ assert.equal(metadata.hasGlobal, config.hasGlobal);
+ }
+
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 87bcb473e7..f853d5873c 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -146,6 +146,10 @@ export async function compile_directory(
if (compiled.css) {
write(`${output_dir}/${file}.css`, compiled.css.code);
+ write(
+ `${output_dir}/${file}.css.json`,
+ JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
+ );
if (output_map) {
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
}
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/labeled-statement-reassign-state/input.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
new file mode 100644
index 0000000000..0b5c13d889
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
new file mode 100644
index 0000000000..c2b36a6e30
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
new file mode 100644
index 0000000000..f81b41d41a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: '
`
);
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();