diff --git a/.changeset/wild-bulldogs-move.md b/.changeset/wild-bulldogs-move.md
new file mode 100644
index 0000000000..c3c5580f77
--- /dev/null
+++ b/.changeset/wild-bulldogs-move.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: allow characters in the supplementary special-purpose plane
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 49e17cd08f..16630a977b 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -250,3 +250,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/05-$props.md b/documentation/docs/02-runes/05-$props.md
index f300fb239d..222b4831b6 100644
--- a/documentation/docs/02-runes/05-$props.md
+++ b/documentation/docs/02-runes/05-$props.md
@@ -37,7 +37,7 @@ On the other side, inside `MyComponent.svelte`, we can receive props with the `$
## Fallback values
-Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop:
+Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop (or the value is `undefined`):
```js
let { adjective = 'happy' } = $props();
@@ -219,4 +219,4 @@ This is useful for linking elements via attributes like `for` and `aria-labelled
-```
\ No newline at end of file
+```
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/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md
index 5e8b4342d3..fe5f8b02aa 100644
--- a/documentation/docs/03-template-syntax/01-basic-markup.md
+++ b/documentation/docs/03-template-syntax/01-basic-markup.md
@@ -154,6 +154,8 @@ A JavaScript expression can be included as text by surrounding it with curly bra
{expression}
```
+Expressions that are `null` or `undefined` will be omitted; all others are [coerced to strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion).
+
Curly braces can be included in a Svelte template by using their [HTML entity](https://developer.mozilla.org/docs/Glossary/Entity) strings: `{`, `{`, or `{` for `{` and `}`, `}`, or `}` for `}`.
If you're using a regular expression (`RegExp`) [literal notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#literal_notation_and_constructor), you'll need to wrap it in parentheses.
diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md
index c9951d3f34..ab536c6e5c 100644
--- a/documentation/docs/03-template-syntax/06-snippet.md
+++ b/documentation/docs/03-template-syntax/06-snippet.md
@@ -112,6 +112,8 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
## Passing snippets to components
+### Explicit props
+
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
```svelte
@@ -144,6 +146,8 @@ Within the template, snippets are values just like any other. As such, they can
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
+### Implicit props
+
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
```svelte
@@ -165,6 +169,8 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
```
+### Implicit `children` snippet
+
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
```svelte
@@ -184,6 +190,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
+### Optional snippet props
+
You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set...
```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-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index a8c39aaf97..e8669ead53 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations
### css_global_block_invalid_list
```
-A `:global` selector cannot be part of a selector list with more than one item
+A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
+```
+
+The following CSS is invalid:
+
+```css
+:global, x {
+ y {
+ color: red;
+ }
+}
+```
+
+This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
+
+```css
+:global {
+ y {
+ color: red;
+ }
+}
+
+x y {
+ color: red;
+}
```
### css_global_block_invalid_modifier
@@ -250,6 +274,12 @@ A `:global` selector cannot modify an existing selector
A `:global` selector can only be modified if it is a descendant of other selectors
```
+### css_global_block_invalid_placement
+
+```
+A `:global` selector cannot be inside a pseudoclass
+```
+
### css_global_invalid_placement
```
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..6c31aaafd0 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
```
@@ -54,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f
```
+### snippet_without_render_tag
+
+```
+Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+```
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+
{label}
+```
+
### store_invalid_shape
```
diff --git a/documentation/docs/98-reference/21-svelte-reactivity.md b/documentation/docs/98-reference/21-svelte-reactivity.md
index 6857c1dba8..8070331f48 100644
--- a/documentation/docs/98-reference/21-svelte-reactivity.md
+++ b/documentation/docs/98-reference/21-svelte-reactivity.md
@@ -2,24 +2,6 @@
title: svelte/reactivity
---
-Svelte provides reactive versions of various built-ins like `SvelteMap`, `SvelteSet` and `SvelteURL`. These can be imported from `svelte/reactivity` and used just like their native counterparts.
-
-```svelte
-
-
-
-
-
-
-
-
-
-
-
-```
+Svelte provides reactive versions of various built-ins like [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) that can be used just like their native counterparts, as well as a handful of additional utilities for handling reactivity.
> MODULE: svelte/reactivity
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 e6b5de7e5f..72cd00bc6a 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,163 @@
# svelte
+## 5.28.2
+
+### Patch Changes
+
+- fix: don't mark selector lists inside `:global` with multiple items as unused ([#15817](https://github.com/sveltejs/svelte/pull/15817))
+
+## 5.28.1
+
+### Patch Changes
+
+- fix: ensure `` properly removes error content in production mode ([#15793](https://github.com/sveltejs/svelte/pull/15793))
+
+- fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` ([#15796](https://github.com/sveltejs/svelte/pull/15796))
+
+- fix: emit error on wrong placement of the `:global` block selector ([#15794](https://github.com/sveltejs/svelte/pull/15794))
+
+## 5.28.0
+
+### Minor Changes
+
+- feat: partially evaluate more expressions ([#15781](https://github.com/sveltejs/svelte/pull/15781))
+
+## 5.27.3
+
+### Patch Changes
+
+- fix: use function declaration for snippets in server output to avoid TDZ violation ([#15789](https://github.com/sveltejs/svelte/pull/15789))
+
+## 5.27.2
+
+### Patch Changes
+
+- chore: use pkg.imports for common modules ([#15787](https://github.com/sveltejs/svelte/pull/15787))
+
+## 5.27.1
+
+### Patch Changes
+
+- chore: default params for html blocks ([#15778](https://github.com/sveltejs/svelte/pull/15778))
+
+- fix: correct suggested type for custom events without detail ([#15763](https://github.com/sveltejs/svelte/pull/15763))
+
+- fix: Throw on unrendered snippets in `dev` ([#15766](https://github.com/sveltejs/svelte/pull/15766))
+
+- fix: avoid unnecessary read version increments ([#15777](https://github.com/sveltejs/svelte/pull/15777))
+
+## 5.27.0
+
+### Minor Changes
+
+- feat: partially evaluate certain expressions ([#15494](https://github.com/sveltejs/svelte/pull/15494))
+
+### Patch Changes
+
+- fix: relax `:global` selector list validation ([#15762](https://github.com/sveltejs/svelte/pull/15762))
+
+## 5.26.3
+
+### Patch Changes
+
+- fix: correctly validate head snippets on the server ([#15755](https://github.com/sveltejs/svelte/pull/15755))
+
+- fix: ignore mutation validation for props that are not proxies in more cases ([#15759](https://github.com/sveltejs/svelte/pull/15759))
+
+- fix: allow self-closing tags within math namespace ([#15761](https://github.com/sveltejs/svelte/pull/15761))
+
+## 5.26.2
+
+### Patch Changes
+
+- fix: correctly validate `undefined` snippet params with default value ([#15750](https://github.com/sveltejs/svelte/pull/15750))
+
+## 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
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-errors/style.md b/packages/svelte/messages/compile-errors/style.md
index 1e1ab45e8c..272efbccce 100644
--- a/packages/svelte/messages/compile-errors/style.md
+++ b/packages/svelte/messages/compile-errors/style.md
@@ -16,7 +16,31 @@
## css_global_block_invalid_list
-> A `:global` selector cannot be part of a selector list with more than one item
+> A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
+
+The following CSS is invalid:
+
+```css
+:global, x {
+ y {
+ color: red;
+ }
+}
+```
+
+This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
+
+```css
+:global {
+ y {
+ color: red;
+ }
+}
+
+x y {
+ color: red;
+}
+```
## css_global_block_invalid_modifier
@@ -26,6 +50,10 @@
> A `:global` selector can only be modified if it is a descendant of other selectors
+## css_global_block_invalid_placement
+
+> A `:global` selector cannot be inside a pseudoclass
+
## css_global_invalid_placement
> `:global(...)` can be at the start or end of a selector sequence, but not in the middle
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..4b4d332202 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
@@ -48,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f
```
+## snippet_without_render_tag
+
+> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+
{label}
+```
+
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 0410520634..ff71429d2f 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.3",
+ "version": "5.28.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@@ -103,6 +103,17 @@
"default": "./src/events/index.js"
}
},
+ "imports": {
+ "#client": "./src/internal/client/types.d.ts",
+ "#client/constants": "./src/internal/client/constants.js",
+ "#compiler": {
+ "types": "./src/compiler/private.d.ts",
+ "default": "./src/compiler/index.js"
+ },
+ "#compiler/builders": "./src/compiler/utils/builders.js",
+ "#server": "./src/internal/server/types.d.ts",
+ "#shared": "./src/internal/shared/types.d.ts"
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/svelte.git",
@@ -156,7 +167,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/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js
index d44afe8205..377fca4343 100644
--- a/packages/svelte/scripts/generate-types.js
+++ b/packages/svelte/scripts/generate-types.js
@@ -24,7 +24,12 @@ await createBundle({
output: `${dir}/types/index.d.ts`,
compilerOptions: {
// so that types/properties with `@internal` (and its dependencies) are removed from the output
- stripInternal: true
+ stripInternal: true,
+ paths: Object.fromEntries(
+ Object.entries(pkg.imports).map(([key, value]) => {
+ return [key, [value.types ?? value.default ?? value]];
+ })
+ )
},
modules: {
[pkg.name]: `${dir}/src/index.d.ts`,
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 6bf973948b..c99f597468 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -555,12 +555,12 @@ export function css_global_block_invalid_declaration(node) {
}
/**
- * A `:global` selector cannot be part of a selector list with more than one item
+ * A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_list(node) {
- e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`);
+ e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`);
}
/**
@@ -581,6 +581,15 @@ export function css_global_block_invalid_modifier_start(node) {
e(node, 'css_global_block_invalid_modifier_start', `A \`:global\` selector can only be modified if it is a descendant of other selectors\nhttps://svelte.dev/e/css_global_block_invalid_modifier_start`);
}
+/**
+ * A `:global` selector cannot be inside a pseudoclass
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function css_global_block_invalid_placement(node) {
+ e(node, 'css_global_block_invalid_placement', `A \`:global\` selector cannot be inside a pseudoclass\nhttps://svelte.dev/e/css_global_block_invalid_placement`);
+}
+
/**
* `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 9d79d88b23..75a9a64905 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -1,7 +1,7 @@
/** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
-/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
+/** @import { Scope } from '../phases/scope.js' */
/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
@@ -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..aba94ee20d 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
@@ -1,7 +1,7 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
-import * as b from '../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../../errors.js';
/**
@@ -24,6 +24,7 @@ const visitors = {
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
+ delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
},
@@ -94,6 +95,9 @@ const visitors = {
TSTypeAliasDeclaration() {
return b.empty;
},
+ TSTypeAssertion(node, context) {
+ return context.visit(node.expression);
+ },
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js
index a68acb996f..a0c2a5b06f 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js
@@ -72,6 +72,8 @@ const NUL = 0;
// to replace them ourselves
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
+// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
+// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
@@ -116,5 +118,10 @@ function validate_code(code) {
return code;
}
+ // supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
+ if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
+ return code;
+ }
+
return NUL;
}
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..d6052c9c3e 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);
}
}
@@ -54,8 +68,12 @@ const css_visitors = {
const global = node.children.find(is_global);
if (global) {
- const idx = node.children.indexOf(global);
+ const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
+ if (is_nested && !global.selectors[0].args) {
+ e.css_global_block_invalid_placement(global.selectors[0]);
+ }
+ const idx = node.children.indexOf(global);
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) {
@@ -99,10 +117,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'
@@ -177,10 +197,12 @@ const css_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
- node.metadata.is_global_block = node.prelude.children.some((selector) => {
+ // We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
+ for (const complex_selector of node.prelude.children) {
let is_global_block = false;
- for (const child of selector.children) {
+ for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
+ const child = complex_selector.children[selector_idx];
const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
@@ -188,70 +210,79 @@ const css_visitors = {
child.metadata.is_global_like = true;
}
- 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) {
- node.metadata.used = true;
- }
- });
- }
- }
- }
+ if (idx === 0) {
+ if (
+ child.selectors.length > 1 &&
+ selector_idx === 0 &&
+ node.metadata.parent_rule === null
+ ) {
+ e.css_global_block_invalid_modifier_start(child.selectors[1]);
+ } else {
+ // `child` starts with `:global`
+ node.metadata.is_global_block = is_global_block = true;
+
+ for (let i = 1; i < child.selectors.length; i++) {
+ walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
+ ComplexSelector(node) {
+ node.metadata.used = true;
+ }
+ });
+ }
- return is_global_block;
- });
+ if (child.combinator && child.combinator.name !== ' ') {
+ e.css_global_block_invalid_combinator(child, child.combinator.name);
+ }
- if (node.metadata.is_global_block) {
- if (node.prelude.children.length > 1) {
- e.css_global_block_invalid_list(node.prelude);
- }
+ const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const is_lone_global =
+ complex_selector.children.length === 1 &&
+ complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
- const complex_selector = node.prelude.children[0];
- const global_selector = complex_selector.children.find((r, selector_idx) => {
- const idx = r.selectors.findIndex(is_global_block_selector);
- if (idx === 0) {
- if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
- e.css_global_block_invalid_modifier_start(r.selectors[1]);
+ if (is_lone_global && node.prelude.children.length > 1) {
+ // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
+ // constrained by `x`, which is not what the user intended
+ e.css_global_block_invalid_list(node.prelude);
+ }
+
+ if (
+ declaration &&
+ // :global { color: red; } is invalid, but foo :global { color: red; } is valid
+ node.prelude.children.length === 1 &&
+ is_lone_global
+ ) {
+ e.css_global_block_invalid_declaration(declaration);
+ }
}
- return true;
} else if (idx !== -1) {
- e.css_global_block_invalid_modifier(r.selectors[idx]);
+ e.css_global_block_invalid_modifier(child.selectors[idx]);
}
- });
-
- if (!global_selector) {
- throw new Error('Internal error: global block without :global selector');
}
- if (global_selector.combinator && global_selector.combinator.name !== ' ') {
- e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
+ if (node.metadata.is_global_block && !is_global_block) {
+ e.css_global_block_invalid_list(node.prelude);
}
+ }
- const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const state = { ...context.state, rule: node };
- if (
- declaration &&
- // :global { color: red; } is invalid, but foo :global { color: red; } is valid
- node.prelude.children.length === 1 &&
- node.prelude.children[0].children.length === 1 &&
- node.prelude.children[0].children[0].selectors.length === 1
- ) {
- e.css_global_block_invalid_declaration(declaration);
- }
+ // 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;
}
- context.next({
- ...context.state,
- rule: node
- });
+ // 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);
- node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
- return selector.children.some(
- ({ metadata }) => !metadata.is_global && !metadata.is_global_like
- );
- });
+ // 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 +320,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..2e36a89649 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -5,8 +5,8 @@
import { walk } from 'zimmerframe';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
-import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
-import * as b from '../../utils/builders.js';
+import { extract_identifiers } from '../../utils/ast.js';
+import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.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/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 995b1d79f8..b788e28737 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -3,10 +3,10 @@
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
-import { get_parent, unwrap_optional } from '../../../utils/ast.js';
+import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {CallExpression} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
index 547f6ab9c7..4b85894e52 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
@@ -1,7 +1,5 @@
-/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */
-/** @import { Binding } from '#compiler' */
+/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { Context } from '../types' */
-/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
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..efbbe6cfa2 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
-/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
@@ -7,6 +6,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 +111,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/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
index 6ea8f238e1..245a164c71 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
@@ -1,10 +1,7 @@
-/** @import { MemberExpression, Node } from 'estree' */
+/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
-import * as w from '../../../warnings.js';
-import { object } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
-import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {MemberExpression} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
index 03dfaebcb7..d5689e5d55 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
@@ -173,7 +173,8 @@ export function RegularElement(node, context) {
if (
context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) &&
- !is_svg(node_name)
+ !is_svg(node_name) &&
+ !is_mathml(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
index d6c74eddb6..12e21c386c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
@@ -1,4 +1,4 @@
-/** @import { AssignmentExpression, Expression, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
+/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
@@ -6,7 +6,7 @@
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
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 0bdfbae746..f0da5a4918 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
@@ -3,7 +3,7 @@
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter, is_state_source } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.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..6d9dac8a33 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 * as b from '#compiler/builders';
+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/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
index 2e051ec674..16f9735370 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
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..e3f9450050 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -1,15 +1,16 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import {
build_assignment_value,
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/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
index 7588b24280..96a4addb72 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
index c563920855..18028fa071 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
@@ -1,7 +1,7 @@
/** @import { Expression, BinaryExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BinaryExpression} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
index 0a49e89cfe..506fd4aafd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { binding_properties } from '../../../bindings.js';
import { build_attribute_value } from './shared/element.js';
import { build_bind_this, validate_binding } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
index 5bfc8a3ef9..d1c0978a81 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
@@ -1,7 +1,7 @@
-/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
+/** @import { ArrowFunctionExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BlockStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
index 66b66c64f2..daa54018c0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
@@ -1,6 +1,6 @@
/** @import { BreakStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BreakStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index aa718b9988..b381f56504 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -1,7 +1,7 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
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 5787b590a8..f3ebd94069 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -1,8 +1,6 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
-/** @import { } from '#compiler' */
/** @import { Context, StateField } from '../types' */
-import { dev, is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
@@ -142,30 +140,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))]
- )
- );
- }
+ // set foo(value) { this.#foo = value; }
+ const val = b.id('value');
- if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
- body.push(
- b.method(
- 'set',
- definition.key,
- [b.id('_')],
- [b.throw_error(`Cannot update a derived property ('${name}')`)]
- )
- );
- }
+ body.push(
+ b.method(
+ 'set',
+ definition.key,
+ [val],
+ [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
+ )
+ );
}
continue;
}
@@ -174,33 +159,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/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
index a10a3da6e3..783bc38e3c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
index 7e33aea435..2f3c0b3d0e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
index d2697fd039..ef9a070859 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
@@ -1,7 +1,7 @@
/** @import { Expression} from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.DebugTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
index 629cacda01..e5aee24765 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
@@ -11,7 +11,7 @@ import {
} from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
index cab7f90c3d..16e400d50c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
@@ -1,6 +1,6 @@
/** @import { ExportNamedDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {ExportNamedDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
index 0424e595be..859842ebc3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
@@ -1,6 +1,6 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 389a694741..b6dca0779a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
@@ -4,7 +4,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { process_children } from './shared/fragment.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
index ed8fefc6ba..5dc8fa5cf9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
@@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {FunctionDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
index 32439879de..a69b9cfe70 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.HtmlTag} node
@@ -11,17 +11,22 @@ import * as b from '../../../../utils/builders.js';
export function HtmlTag(node, context) {
context.state.template.push('');
- // push into init, so that bindings run afterwards, which might trigger another run and override hydration
- context.state.init.push(
- b.stmt(
- b.call(
- '$.html',
- context.state.node,
- b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
- b.literal(context.state.metadata.namespace === 'svg'),
- b.literal(context.state.metadata.namespace === 'mathml'),
- is_ignored(node, 'hydration_html_changed') && b.true
- )
+ const expression = /** @type {Expression} */ (context.visit(node.expression));
+
+ const is_svg = context.state.metadata.namespace === 'svg';
+ const is_mathml = context.state.metadata.namespace === 'mathml';
+
+ const statement = b.stmt(
+ b.call(
+ '$.html',
+ context.state.node,
+ b.thunk(expression),
+ is_svg && b.true,
+ is_mathml && b.true,
+ is_ignored(node, 'hydration_html_changed') && b.true
)
);
+
+ // push into init, so that bindings run afterwards, which might trigger another run and override hydration
+ context.state.init.push(statement);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
index ae62909eff..b01ed01bd7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
@@ -1,7 +1,7 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
index fdd21b2b7e..c650a1e15c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
@@ -1,7 +1,7 @@
-/** @import { BlockStatement, Expression, Identifier } from 'estree' */
+/** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.IfBlock} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
index 29700246d4..b572e1d17f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
@@ -1,6 +1,6 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {ImportDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
index a013827f60..7d6a8b0006 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.KeyBlock} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
index 3c8f57f46b..8d24d260c5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
@@ -1,9 +1,7 @@
-/** @import { Location } from 'locate-character' */
/** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import { dev, is_ignored, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
index e174073a26..abdbc381d9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
index 3f2aada1f5..ab88345ddd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
@@ -1,6 +1,6 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {MemberExpression} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
index 7c2b1209e9..7a66a8ecbb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
index 29403ca6ed..07342da314 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
@@ -1,7 +1,7 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
index 45a594af1f..7468fcbbc7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
@@ -1,4 +1,4 @@
-/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
+/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -13,7 +13,7 @@ import {
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
@@ -685,14 +685,13 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
: value
);
+ const evaluated = context.state.scope.evaluate(value);
+ const assignment = b.assignment('=', b.member(node_id, '__value'), value);
+
const inner_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
- b.conditional(
- b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)),
- b.literal(''), // render null/undefined values as empty string to support placeholder options
- value
- )
+ evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
);
const update = b.stmt(
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
index 33ae6d4d2b..6067c2562a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.RenderTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
index c6f4ba1ed3..ba9fcc7377 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js';
import { memoize_expression } from './shared/utils.js';
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..a82645cd7a 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,9 +1,9 @@
-/** @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';
import { extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.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} */
@@ -21,6 +21,10 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
+ if (dev) {
+ declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
+ }
+
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
@@ -30,12 +34,7 @@ export function SnippetBlock(node, context) {
if (!argument) continue;
if (argument.type === 'Identifier') {
- args.push({
- type: 'AssignmentPattern',
- left: argument,
- right: b.id('$.noop')
- });
-
+ args.push(b.assignment_pattern(argument, b.id('$.noop')));
transform[argument.name] = { read: b.call };
continue;
@@ -72,12 +71,10 @@ export function SnippetBlock(node, context) {
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
]);
- /** @type {Expression} */
- let snippet = b.arrow(args, body);
-
- if (dev) {
- snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
- }
+ // in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
+ let snippet = dev
+ ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
+ : b.arrow(args, body);
const declaration = b.const(node.expression, snippet);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
index 9228df9703..b279b5badd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteBoundary} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
index 115eb6ccc1..ee597dd043 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
@@ -3,10 +3,10 @@
/** @import { ComponentContext } from '../types' */
import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
-import { build_render_statement, get_expression_id } from './shared/utils.js';
+import { build_render_statement } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
index 25fcff0631..0701c37c48 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteHead} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
index 72cc57b068..7bfdaf1850 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_template_chunk } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
index e331f36472..41340c1290 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../../constants.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
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..96be119b84 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 * as b from '#compiler/builders';
+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/UseDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
index be9eb2d516..b95f2fc3ef 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index 3a914fb560..84044e4ded 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -3,7 +3,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
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..c4071c67fe 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
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
@@ -179,19 +179,29 @@ export function build_component(node, component_name, context, anchor = context.
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
- if (dev && attribute.name !== 'this') {
- binding_initializers.push(
- b.stmt(
- b.call(
- b.id('$.add_owner_effect'),
- expression.type === 'SequenceExpression'
- ? expression.expressions[0]
- : b.thunk(expression),
- b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ if (
+ dev &&
+ attribute.name !== 'this' &&
+ !is_ignored(node, 'ownership_invalid_binding') &&
+ // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation
+ attribute.expression.type !== 'SequenceExpression'
+ ) {
+ const left = object(attribute.expression);
+ const binding = left && context.state.scope.get(left.name);
+
+ if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') {
+ context.state.analysis.needs_mutation_validation = true;
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ '$$ownership_validator.binding',
+ b.literal(binding.node.name),
+ b.id(component_name),
+ b.thunk(expression)
+ )
)
- )
- );
+ );
+ }
}
if (expression.type === 'SequenceExpression') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
index a13ecfed2c..f6bb26daac 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
@@ -1,7 +1,7 @@
/** @import { Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */
import { is_state_source } from '../../utils.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* Turns `foo` into `$.get(foo)`
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
index 97cec7a729..a093a0bf4a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
@@ -1,11 +1,11 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
-/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
+/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
index 2667a96f6a..d252bd5474 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.Attribute} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
index f076d7c11e..c91e2b3b44 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types' */
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
index 558bc4fee7..c878f2fc07 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
*
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..bc79b76043 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 * as b from '#compiler/builders';
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';
/**
@@ -69,11 +69,17 @@ export function build_template_chunk(
node.metadata.expression
);
- has_state ||= node.metadata.expression.has_state;
+ const evaluated = state.scope.evaluate(value);
+
+ has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
+ if (evaluated.is_known) {
+ value = b.literal(evaluated.value);
+ }
+
return { value, has_state };
}
@@ -89,21 +95,19 @@ export function build_template_chunk(
}
}
- const is_defined =
- value.type === 'BinaryExpression' ||
- (value.type === 'UnaryExpression' && value.operator !== 'void') ||
- (value.type === 'LogicalExpression' && value.right.type === 'Literal') ||
- (value.type === 'Identifier' && value.name === state.analysis.props_id?.name);
-
- if (!is_defined) {
- // add `?? ''` where necessary (TODO optimise more cases)
- value = b.logical('??', value, b.literal(''));
- }
+ if (evaluated.is_known) {
+ quasi.value.cooked += evaluated.value + '';
+ } else {
+ if (!evaluated.is_defined) {
+ // add `?? ''` where necessary
+ value = b.logical('??', value, b.literal(''));
+ }
- expressions.push(value);
+ expressions.push(value);
- quasi = b.quasi('', i + 1 === values.length);
- quasis.push(quasi);
+ quasi = b.quasi('', i + 1 === values.length);
+ quasis.push(quasi);
+ }
}
}
@@ -295,3 +299,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..cee7ab2791 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]);
@@ -169,7 +170,11 @@ const visitors = {
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
- if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
+ if (
+ node.prelude.children.length === 1 &&
+ selector.children.length === 1 &&
+ selector.children[0].selectors.length === 1
+ ) {
// `:global {...}`
if (state.minify) {
state.code.remove(node.start, node.block.start + 1);
@@ -191,9 +196,12 @@ const visitors = {
next();
},
SelectorList(node, { state, next, path }) {
+ const parent = path.at(-1);
+
// Only add comments if we're not inside a complex selector that itself is unused or a global block
if (
- !is_in_global_block(path) &&
+ (!is_in_global_block(path) ||
+ (node.children.length > 1 && parent?.type === 'Rule' && parent.metadata.is_global_block)) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children;
@@ -255,7 +263,6 @@ const visitors = {
// if this selector list belongs to a rule, require a specificity bump for the
// first scoped selector but only if we're at the top level
- let parent = path.at(-1);
if (parent?.type === 'Rule') {
specificity = { bumped: false };
@@ -281,13 +288,24 @@ const visitors = {
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
- if (
- node.metadata.rule?.metadata.parent_rule &&
- global.args === null &&
- relative_selector.combinator === null
- ) {
- // div { :global.x { ... } } becomes div { &.x { ... } }
- context.state.code.prependRight(global.start, '&');
+ const parent_rule = node.metadata.rule?.metadata.parent_rule;
+ if (parent_rule && global.args === null) {
+ if (relative_selector.combinator === null) {
+ // div { :global.x { ... } } becomes div { &.x { ... } }
+ context.state.code.prependRight(global.start, '&');
+ }
+
+ // In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if
+ // the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector
+ if (
+ parent_rule.prelude.children.length > 1 &&
+ node.children.length === node.children.findIndex((s) => s === relative_selector) - 1
+ ) {
+ const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end);
+ if (next_selector && next_selector.metadata.used) {
+ context.state.code.update(global.end, next_selector.start, '');
+ }
+ }
}
continue;
} else {
@@ -360,7 +378,6 @@ const visitors = {
};
/**
- *
* @param {Array} path
*/
function is_in_global_block(path) {
@@ -379,7 +396,9 @@ function remove_global_pseudo_class(selector, combinator, state) {
// div :global.x becomes div.x
while (/\s/.test(state.code.original[start - 1])) start--;
}
- state.code.remove(start, selector.start + ':global'.length);
+
+ // update(...), not remove(...) because there could be a closing unused comment at the end
+ state.code.update(start, selector.start + ':global'.length, '');
} else {
state.code
.remove(selector.start, selector.start + ':global('.length)
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..e7896991d9 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
@@ -5,7 +5,7 @@
import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.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/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
index 6364063b3b..071a12f9bc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
@@ -1,7 +1,7 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
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..35e431f43d 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
@@ -1,8 +1,8 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
-import { empty_comment } from './shared/utils.js';
+import * as b from '#compiler/builders';
+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/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
index ad573b0329..d5097578bd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
@@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
index 365084a284..c0ebdeae08 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
@@ -2,7 +2,7 @@
/** @import { Context } from '../types.js' */
/** @import { StateField } from '../../client/types.js' */
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
index 01bf50fafa..503b380c50 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_inline_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
index 5f6aea8338..a8e4e575cc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
@@ -1,7 +1,7 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.ConstTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
index 6fe4e0b82b..31b53fd3eb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.DebugTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
index 104f1f2405..ac6c9891a7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
@@ -1,8 +1,8 @@
-/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
+/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
index 00d0dba5da..f77e19aec2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
@@ -1,6 +1,6 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
index a293b98e7e..a1d25980c4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
@@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
import { clean_nodes, infer_namespace } from '../../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment, process_children, build_template } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
index 0d551a884a..9e857a9308 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.HtmlTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
index b8c2699d54..fa887650b3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
@@ -1,7 +1,7 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types.js' */
import is_reference from 'is-reference';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
index cbdd2cd8cc..eb51c941f5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
@@ -1,8 +1,8 @@
-/** @import { BlockStatement, Expression, IfStatement } from 'estree' */
+/** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
index 2b394e94e3..83c828b839 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
@@ -1,6 +1,6 @@
/** @import { ExpressionStatement, LabeledStatement } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {LabeledStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
index 527c8cf6ed..73631395e6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
@@ -1,6 +1,6 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {MemberExpression} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
index 04751a19d1..c9225bb8da 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
@@ -1,6 +1,6 @@
/** @import { Expression, PropertyDefinition } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
index af50695efa..5901cb4c50 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
@@ -4,7 +4,7 @@
/** @import { Scope } from '../../../scope.js' */
import { is_void } from '../../../../../utils.js';
import { dev, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { process_children, build_template } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
index ebf8c3be1c..dd2ede3ba3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
index e7925071cd..fee7cb6e02 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment, build_attribute_value } from './shared/utils.js';
/**
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..238485e665 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,14 +1,15 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import { dev } from '../../../../state.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SnippetBlock} node
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
- const fn = b.function_declaration(
+ let fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
@@ -17,9 +18,12 @@ export function SnippetBlock(node, context) {
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
- if (node.metadata.can_hoist) {
- context.state.hoisted.push(fn);
- } else {
- context.state.init.push(fn);
+ const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
+
+ if (dev) {
+ fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
+ statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
}
+
+ statements.push(fn);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
index 0d54feee11..734740c1b1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteBoundary} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
index 9f6faa33c8..fd16219860 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
@@ -3,7 +3,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
index 7da7d8355c..7d064ffbf5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteHead} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
index fbedcff283..bb0ecb21b2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_inline_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
index 8fd1973453..c42df4c646 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { process_children, build_template } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
index ae78e14c13..8a2f874e22 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
@@ -1,6 +1,6 @@
/** @import { UpdateExpression } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {UpdateExpression} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
index a9c9777335..1f2bd3e2b1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
@@ -3,7 +3,7 @@
/** @import { Context } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { build_fallback, extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index 695161ff9b..9bccf9e05e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -2,8 +2,9 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
+import { dev } from '../../../../../state.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -238,7 +239,13 @@ export function build_inline_component(node, expression, context) {
)
) {
// create `children` prop...
- push_prop(b.prop('init', b.id('children'), slot_fn));
+ push_prop(
+ b.prop(
+ 'init',
+ b.id('children'),
+ dev ? b.call('$.prevent_snippet_stringification', slot_fn) : slot_fn
+ )
+ );
// and `$$slots.default: true` so that `` on the child works
serialized_slots.push(b.init(slot_name, b.true));
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
index 4a5becfb2f..b0bcb8fd6f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
@@ -1,11 +1,7 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
-/** @import { AST, Namespace } from '#compiler' */
+/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
-import {
- get_attribute_chunks,
- is_event_attribute,
- is_text_attribute
-} from '../../../../../utils/ast.js';
+import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
import {
create_attribute,
@@ -13,7 +9,7 @@ import {
is_custom_element_node
} from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import {
ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
index 2c6aa2f316..8fcf8efa68 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
@@ -1,4 +1,4 @@
-/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
+/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
@@ -8,7 +8,7 @@ import {
BLOCK_OPEN,
EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
@@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) {
if (node.type === 'Text' || node.type === 'Comment') {
quasi.value.cooked +=
node.type === 'Comment' ? `` : escape_html(node.data);
- } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
- if (node.expression.value != null) {
- quasi.value.cooked += escape_html(node.expression.value + '');
- }
} else {
- expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
+ const evaluated = state.scope.evaluate(node.expression);
+
+ if (evaluated.is_known) {
+ quasi.value.cooked += escape_html((evaluated.value ?? '') + '');
+ } else {
+ expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
- quasi = b.quasi('', i + 1 === sequence.length);
- quasis.push(quasi);
+ quasi = b.quasi('', i + 1 === sequence.length);
+ quasis.push(quasi);
+ }
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
index e8e02b8f58..3e6bb0c4c6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
@@ -2,7 +2,7 @@
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @template {ClientContext | ServerContext} Context
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 46872fbfcf..5aa40c8abb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -8,7 +8,7 @@ import {
regex_starts_with_newline,
regex_starts_with_whitespaces
} from '../patterns.js';
-import * as b from '../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index b6063c3234..8297f174d3 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -1,10 +1,10 @@
-/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
+/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
-import * as b from '../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../errors.js';
import {
extract_identifiers,
@@ -16,6 +16,74 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
+const UNKNOWN = Symbol('unknown');
+/** Includes `BigInt` */
+export const NUMBER = Symbol('number');
+export const STRING = Symbol('string');
+
+/** @type {Record} */
+const globals = {
+ BigInt: [NUMBER, BigInt],
+ 'Math.min': [NUMBER, Math.min],
+ 'Math.max': [NUMBER, Math.max],
+ 'Math.random': [NUMBER],
+ 'Math.floor': [NUMBER, Math.floor],
+ // @ts-expect-error
+ 'Math.f16round': [NUMBER, Math.f16round],
+ 'Math.round': [NUMBER, Math.round],
+ 'Math.abs': [NUMBER, Math.abs],
+ 'Math.acos': [NUMBER, Math.acos],
+ 'Math.asin': [NUMBER, Math.asin],
+ 'Math.atan': [NUMBER, Math.atan],
+ 'Math.atan2': [NUMBER, Math.atan2],
+ 'Math.ceil': [NUMBER, Math.ceil],
+ 'Math.cos': [NUMBER, Math.cos],
+ 'Math.sin': [NUMBER, Math.sin],
+ 'Math.tan': [NUMBER, Math.tan],
+ 'Math.exp': [NUMBER, Math.exp],
+ 'Math.log': [NUMBER, Math.log],
+ 'Math.pow': [NUMBER, Math.pow],
+ 'Math.sqrt': [NUMBER, Math.sqrt],
+ 'Math.clz32': [NUMBER, Math.clz32],
+ 'Math.imul': [NUMBER, Math.imul],
+ 'Math.sign': [NUMBER, Math.sign],
+ 'Math.log10': [NUMBER, Math.log10],
+ 'Math.log2': [NUMBER, Math.log2],
+ 'Math.log1p': [NUMBER, Math.log1p],
+ 'Math.expm1': [NUMBER, Math.expm1],
+ 'Math.cosh': [NUMBER, Math.cosh],
+ 'Math.sinh': [NUMBER, Math.sinh],
+ 'Math.tanh': [NUMBER, Math.tanh],
+ 'Math.acosh': [NUMBER, Math.acosh],
+ 'Math.asinh': [NUMBER, Math.asinh],
+ 'Math.atanh': [NUMBER, Math.atanh],
+ 'Math.trunc': [NUMBER, Math.trunc],
+ 'Math.fround': [NUMBER, Math.fround],
+ 'Math.cbrt': [NUMBER, Math.cbrt],
+ Number: [NUMBER, Number],
+ 'Number.isInteger': [NUMBER, Number.isInteger],
+ 'Number.isFinite': [NUMBER, Number.isFinite],
+ 'Number.isNaN': [NUMBER, Number.isNaN],
+ 'Number.isSafeInteger': [NUMBER, Number.isSafeInteger],
+ 'Number.parseFloat': [NUMBER, Number.parseFloat],
+ 'Number.parseInt': [NUMBER, Number.parseInt],
+ String: [STRING, String],
+ 'String.fromCharCode': [STRING, String.fromCharCode],
+ 'String.fromCodePoint': [STRING, String.fromCodePoint]
+};
+
+/** @type {Record} */
+const global_constants = {
+ 'Math.PI': Math.PI,
+ 'Math.E': Math.E,
+ 'Math.LN10': Math.LN10,
+ 'Math.LN2': Math.LN2,
+ 'Math.LOG10E': Math.LOG10E,
+ 'Math.LOG2E': Math.LOG2E,
+ 'Math.SQRT2': Math.SQRT2,
+ 'Math.SQRT1_2': Math.SQRT1_2
+};
+
export class Binding {
/** @type {Scope} */
scope;
@@ -34,7 +102,7 @@ export class Binding {
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
* @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
*/
- initial;
+ initial = null;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
@@ -100,6 +168,365 @@ export class Binding {
}
}
+class Evaluation {
+ /** @type {Set} */
+ values;
+
+ /**
+ * True if there is exactly one possible value
+ * @readonly
+ * @type {boolean}
+ */
+ is_known = true;
+
+ /**
+ * True if the value is known to not be null/undefined
+ * @readonly
+ * @type {boolean}
+ */
+ is_defined = true;
+
+ /**
+ * True if the value is known to be a string
+ * @readonly
+ * @type {boolean}
+ */
+ is_string = true;
+
+ /**
+ * True if the value is known to be a number
+ * @readonly
+ * @type {boolean}
+ */
+ is_number = true;
+
+ /**
+ * @readonly
+ * @type {any}
+ */
+ value = undefined;
+
+ /**
+ *
+ * @param {Scope} scope
+ * @param {Expression} expression
+ * @param {Set} values
+ */
+ constructor(scope, expression, values) {
+ this.values = values;
+
+ switch (expression.type) {
+ case 'Literal': {
+ this.values.add(expression.value);
+ break;
+ }
+
+ case 'Identifier': {
+ const binding = scope.get(expression.name);
+
+ if (binding) {
+ if (
+ binding.initial?.type === 'CallExpression' &&
+ get_rune(binding.initial, scope) === '$props.id'
+ ) {
+ this.values.add(STRING);
+ break;
+ }
+
+ const is_prop =
+ binding.kind === 'prop' ||
+ binding.kind === 'rest_prop' ||
+ binding.kind === 'bindable_prop';
+
+ if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) {
+ this.values.add(NUMBER);
+ break;
+ }
+
+ if (!binding.updated && binding.initial !== null && !is_prop) {
+ binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
+ break;
+ }
+ } else if (expression.name === 'undefined') {
+ this.values.add(undefined);
+ break;
+ }
+
+ // TODO glean what we can from reassignments
+ // TODO one day, expose props and imports somehow
+
+ this.values.add(UNKNOWN);
+ break;
+ }
+
+ case 'BinaryExpression': {
+ const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in`
+ const b = scope.evaluate(expression.right);
+
+ if (a.is_known && b.is_known) {
+ this.values.add(binary[expression.operator](a.value, b.value));
+ break;
+ }
+
+ switch (expression.operator) {
+ case '!=':
+ case '!==':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ case '==':
+ case '===':
+ case 'in':
+ case 'instanceof':
+ this.values.add(true);
+ this.values.add(false);
+ break;
+
+ case '%':
+ case '&':
+ case '*':
+ case '**':
+ case '-':
+ case '/':
+ case '<<':
+ case '>>':
+ case '>>>':
+ case '^':
+ case '|':
+ this.values.add(NUMBER);
+ break;
+
+ case '+':
+ if (a.is_string || b.is_string) {
+ this.values.add(STRING);
+ } else if (a.is_number && b.is_number) {
+ this.values.add(NUMBER);
+ } else {
+ this.values.add(STRING);
+ this.values.add(NUMBER);
+ }
+ break;
+
+ default:
+ this.values.add(UNKNOWN);
+ }
+ break;
+ }
+
+ case 'ConditionalExpression': {
+ const test = scope.evaluate(expression.test);
+ const consequent = scope.evaluate(expression.consequent);
+ const alternate = scope.evaluate(expression.alternate);
+
+ if (test.is_known) {
+ for (const value of (test.value ? consequent : alternate).values) {
+ this.values.add(value);
+ }
+ } else {
+ for (const value of consequent.values) {
+ this.values.add(value);
+ }
+
+ for (const value of alternate.values) {
+ this.values.add(value);
+ }
+ }
+ break;
+ }
+
+ case 'LogicalExpression': {
+ const a = scope.evaluate(expression.left);
+ const b = scope.evaluate(expression.right);
+
+ if (a.is_known) {
+ if (b.is_known) {
+ this.values.add(logical[expression.operator](a.value, b.value));
+ break;
+ }
+
+ if (
+ (expression.operator === '&&' && !a.value) ||
+ (expression.operator === '||' && a.value) ||
+ (expression.operator === '??' && a.value != null)
+ ) {
+ this.values.add(a.value);
+ } else {
+ for (const value of b.values) {
+ this.values.add(value);
+ }
+ }
+
+ break;
+ }
+
+ for (const value of a.values) {
+ this.values.add(value);
+ }
+
+ for (const value of b.values) {
+ this.values.add(value);
+ }
+ break;
+ }
+
+ case 'UnaryExpression': {
+ const argument = scope.evaluate(expression.argument);
+
+ if (argument.is_known) {
+ this.values.add(unary[expression.operator](argument.value));
+ break;
+ }
+
+ switch (expression.operator) {
+ case '!':
+ case 'delete':
+ this.values.add(false);
+ this.values.add(true);
+ break;
+
+ case '+':
+ case '-':
+ case '~':
+ this.values.add(NUMBER);
+ break;
+
+ case 'typeof':
+ this.values.add(STRING);
+ break;
+
+ case 'void':
+ this.values.add(undefined);
+ break;
+
+ default:
+ this.values.add(UNKNOWN);
+ }
+ break;
+ }
+
+ case 'CallExpression': {
+ const keypath = get_global_keypath(expression.callee, scope);
+
+ if (keypath) {
+ if (is_rune(keypath)) {
+ const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
+
+ switch (keypath) {
+ case '$state':
+ case '$state.raw':
+ case '$derived':
+ if (arg) {
+ scope.evaluate(arg, this.values);
+ } else {
+ this.values.add(undefined);
+ }
+ break;
+
+ case '$props.id':
+ this.values.add(STRING);
+ break;
+
+ case '$effect.tracking':
+ this.values.add(false);
+ this.values.add(true);
+ break;
+
+ case '$derived.by':
+ if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') {
+ scope.evaluate(arg.body, this.values);
+ break;
+ }
+
+ this.values.add(UNKNOWN);
+ break;
+
+ default: {
+ this.values.add(UNKNOWN);
+ }
+ }
+
+ break;
+ }
+
+ if (
+ Object.hasOwn(globals, keypath) &&
+ expression.arguments.every((arg) => arg.type !== 'SpreadElement')
+ ) {
+ const [type, fn] = globals[keypath];
+ const values = expression.arguments.map((arg) => scope.evaluate(arg));
+
+ if (fn && values.every((e) => e.is_known)) {
+ this.values.add(fn(...values.map((e) => e.value)));
+ } else {
+ this.values.add(type);
+ }
+
+ break;
+ }
+ }
+
+ this.values.add(UNKNOWN);
+ break;
+ }
+
+ case 'TemplateLiteral': {
+ let result = expression.quasis[0].value.cooked;
+
+ for (let i = 0; i < expression.expressions.length; i += 1) {
+ const e = scope.evaluate(expression.expressions[i]);
+
+ if (e.is_known) {
+ result += e.value + expression.quasis[i + 1].value.cooked;
+ } else {
+ this.values.add(STRING);
+ break;
+ }
+ }
+
+ this.values.add(result);
+ break;
+ }
+
+ case 'MemberExpression': {
+ const keypath = get_global_keypath(expression, scope);
+
+ if (keypath && Object.hasOwn(global_constants, keypath)) {
+ this.values.add(global_constants[keypath]);
+ break;
+ }
+
+ this.values.add(UNKNOWN);
+ break;
+ }
+
+ default: {
+ this.values.add(UNKNOWN);
+ }
+ }
+
+ for (const value of this.values) {
+ this.value = value; // saves having special logic for `size === 1`
+
+ if (value !== STRING && typeof value !== 'string') {
+ this.is_string = false;
+ }
+
+ if (value !== NUMBER && typeof value !== 'number') {
+ this.is_number = false;
+ }
+
+ if (value == null || value === UNKNOWN) {
+ this.is_defined = false;
+ }
+ }
+
+ if (this.values.size > 1 || typeof this.value === 'symbol') {
+ this.is_known = false;
+ }
+ }
+}
+
export class Scope {
/** @type {ScopeRoot} */
root;
@@ -279,8 +706,63 @@ export class Scope {
this.root.conflicts.add(node.name);
}
}
+
+ /**
+ * Does partial evaluation to find an exact value or at least the rough type of the expression.
+ * Only call this once scope has been fully generated in a first pass,
+ * else this evaluates on incomplete data and may yield wrong results.
+ * @param {Expression} expression
+ * @param {Set} [values]
+ */
+ evaluate(expression, values = new Set()) {
+ return new Evaluation(this, expression, values);
+ }
}
+/** @type {Record any>} */
+const binary = {
+ '!=': (left, right) => left != right,
+ '!==': (left, right) => left !== right,
+ '<': (left, right) => left < right,
+ '<=': (left, right) => left <= right,
+ '>': (left, right) => left > right,
+ '>=': (left, right) => left >= right,
+ '==': (left, right) => left == right,
+ '===': (left, right) => left === right,
+ in: (left, right) => left in right,
+ instanceof: (left, right) => left instanceof right,
+ '%': (left, right) => left % right,
+ '&': (left, right) => left & right,
+ '*': (left, right) => left * right,
+ '**': (left, right) => left ** right,
+ '+': (left, right) => left + right,
+ '-': (left, right) => left - right,
+ '/': (left, right) => left / right,
+ '<<': (left, right) => left << right,
+ '>>': (left, right) => left >> right,
+ '>>>': (left, right) => left >>> right,
+ '^': (left, right) => left ^ right,
+ '|': (left, right) => left | right
+};
+
+/** @type {Record any>} */
+const unary = {
+ '-': (argument) => -argument,
+ '+': (argument) => +argument,
+ '!': (argument) => !argument,
+ '~': (argument) => ~argument,
+ typeof: (argument) => typeof argument,
+ void: () => undefined,
+ delete: () => true
+};
+
+/** @type {Record any>} */
+const logical = {
+ '||': (left, right) => left || right,
+ '&&': (left, right) => left && right,
+ '??': (left, right) => left ?? right
+};
+
export class ScopeRoot {
/** @type {Set} */
conflicts = new Set();
@@ -797,7 +1279,19 @@ export function get_rune(node, scope) {
if (!node) return null;
if (node.type !== 'CallExpression') return null;
- let n = node.callee;
+ const keypath = get_global_keypath(node.callee, scope);
+
+ if (!keypath || !is_rune(keypath)) return null;
+ return keypath;
+}
+
+/**
+ * Returns the name of the rune if the given expression is a `CallExpression` using a rune.
+ * @param {Expression | Super} node
+ * @param {Scope} scope
+ */
+function get_global_keypath(node, scope) {
+ let n = node;
let joined = '';
@@ -815,12 +1309,8 @@ export function get_rune(node, scope) {
if (n.type !== 'Identifier') return null;
- joined = n.name + joined;
-
- if (!is_rune(joined)) return null;
-
const binding = scope.get(n.name);
if (binding !== null) return null; // rune name, but references a variable or store
- return joined;
+ return n.name + joined;
}
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/private.d.ts b/packages/svelte/src/compiler/private.d.ts
new file mode 100644
index 0000000000..7ac1089373
--- /dev/null
+++ b/packages/svelte/src/compiler/private.d.ts
@@ -0,0 +1,2 @@
+export * from './types/index';
+export * from './index';
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/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index 0a24d2ff0d..108f4eff64 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
-import * as b from '../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* Gets the left-most identifier of a member expression or identifier.
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/index-client.js b/packages/svelte/src/index-client.js
index fd8e999da7..efd5628ae9 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -114,7 +114,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
* ```ts
* const dispatch = createEventDispatcher<{
- * loaded: never; // does not take a detail argument
+ * loaded: null; // does not take a detail argument
* change: string; // takes a detail argument of type string, which is required
* optional: number | null; // takes an optional detail argument of type number
* }>();
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 21377c1cc8..7e5196c606 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -23,6 +23,5 @@ export const EFFECT_HAS_DERIVED = 1 << 20;
export const EFFECT_IS_UPDATING = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
-export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js
index bfca9d5e6a..7c7213b7a2 100644
--- a/packages/svelte/src/internal/client/context.js
+++ b/packages/svelte/src/internal/client/context.js
@@ -1,15 +1,13 @@
/** @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 {
active_effect,
active_reaction,
set_active_effect,
- set_active_reaction,
- untrack
+ set_active_reaction
} from './runtime.js';
import { effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
@@ -67,15 +65,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/console-log.js b/packages/svelte/src/internal/client/dev/console-log.js
index a578ecea45..d314359ef6 100644
--- a/packages/svelte/src/internal/client/dev/console-log.js
+++ b/packages/svelte/src/internal/client/dev/console-log.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { snapshot } from '../../shared/clone.js';
import * as w from '../warnings.js';
import { untrack } from '../runtime.js';
diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js
new file mode 100644
index 0000000000..fbde87a2d7
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/debug.js
@@ -0,0 +1,109 @@
+/** @import { Derived, Effect, Value } from '#client' */
+
+import {
+ BLOCK_EFFECT,
+ BOUNDARY_EFFECT,
+ BRANCH_EFFECT,
+ CLEAN,
+ DERIVED,
+ EFFECT,
+ MAYBE_DIRTY,
+ RENDER_EFFECT,
+ ROOT_EFFECT
+} from '#client/constants';
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function root(effect) {
+ while (effect.parent !== null) {
+ effect = effect.parent;
+ }
+
+ return effect;
+}
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function log_effect_tree(effect, depth = 0) {
+ const flags = effect.f;
+
+ let label = '(unknown)';
+
+ if ((flags & ROOT_EFFECT) !== 0) {
+ label = 'root';
+ } else if ((flags & BOUNDARY_EFFECT) !== 0) {
+ label = 'boundary';
+ } else if ((flags & BLOCK_EFFECT) !== 0) {
+ label = 'block';
+ } else if ((flags & BRANCH_EFFECT) !== 0) {
+ label = 'branch';
+ } else if ((flags & RENDER_EFFECT) !== 0) {
+ label = 'render effect';
+ } else if ((flags & EFFECT) !== 0) {
+ label = 'effect';
+ }
+
+ let status =
+ (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
+
+ // eslint-disable-next-line no-console
+ console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`);
+
+ if (depth === 0) {
+ const callsite = new Error().stack
+ ?.split('\n')[2]
+ .replace(/\s+at (?: \w+\(?)?(.+)\)?/, (m, $1) => $1.replace(/\?[^:]+/, ''));
+
+ // eslint-disable-next-line no-console
+ console.log(callsite);
+ }
+
+ if (effect.deps !== null) {
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cdeps', 'font-weight: normal');
+
+ for (const dep of effect.deps) {
+ log_dep(dep);
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ }
+
+ let child = effect.first;
+ while (child !== null) {
+ log_effect_tree(child, depth + 1);
+ child = child.next;
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+}
+
+/**
+ *
+ * @param {Value} dep
+ */
+function log_dep(dep) {
+ if ((dep.f & DERIVED) !== 0) {
+ const derived = /** @type {Derived} */ (dep);
+
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cderived', 'font-weight: normal', derived.v);
+ if (derived.deps) {
+ for (const d of derived.deps) {
+ log_dep(d);
+ }
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('state', dep.v);
+ }
+}
diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js
index ee5e08c0b1..27e2643d16 100644
--- a/packages/svelte/src/internal/client/dev/hmr.js
+++ b/packages/svelte/src/internal/client/dev/hmr.js
@@ -1,6 +1,6 @@
/** @import { Source, Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
-import { EFFECT_TRANSPARENT } from '../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js';
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..19d2cdb343 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -1,304 +1,81 @@
-/** @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 '#client/constants';
+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
- });
- }
-}
+ /** @type {any} */
+ let value = props;
-/**
- * @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 = 0; i < path.length - 1; i++) {
+ value = value[path[i]];
+ if (!value?.[STATE_SYMBOL]) {
+ return result;
+ }
}
- }
- } 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/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 3881ef3442..ad80e75c3d 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -1,8 +1,8 @@
-/** @import { Derived, Reaction, Signal, Value } from '#client' */
+/** @import { Derived, Reaction, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
-import { DERIVED, STATE_SYMBOL } from '../constants.js';
+import { DERIVED, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
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/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index c1ca7a9600..53060017b9 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode, } from '#client' */
-import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
+import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js
index 473d35b122..ecbcfd3e83 100644
--- a/packages/svelte/src/internal/client/dom/blocks/css-props.js
+++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js
@@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */
import { render_effect, teardown } from '../../reactivity/effects.js';
-import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
+import { hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 3baa03a917..92c953b541 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -33,9 +33,9 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
-import { INERT } from '../../constants.js';
+import { INERT } from '#client/constants';
import { queue_micro_task } from '../task.js';
-import { active_effect, active_reaction, get } from '../../runtime.js';
+import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index b3fc5a9c72..92c8243478 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js';
-import { block, branch, destroy_effect } from '../../reactivity/effects.js';
+import { remove_effect_dom, template_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
@@ -9,6 +9,7 @@ import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
+import { active_effect } from '../../runtime.js';
/**
* @param {Element} element
@@ -34,89 +35,81 @@ function check_hash(element, server_hash, value) {
/**
* @param {Element | Text | Comment} node
* @param {() => string} get_value
- * @param {boolean} svg
- * @param {boolean} mathml
+ * @param {boolean} [svg]
+ * @param {boolean} [mathml]
* @param {boolean} [skip_warning]
* @returns {void}
*/
-export function html(node, get_value, svg, mathml, skip_warning) {
+export function html(node, get_value, svg = false, mathml = false, skip_warning = false) {
var anchor = node;
var value = '';
- /** @type {Effect | undefined} */
- var effect;
+ template_effect(() => {
+ var effect = /** @type {Effect} */ (active_effect);
- block(() => {
if (value === (value = get_value() ?? '')) {
- if (hydrating) {
- hydrate_next();
- }
+ if (hydrating) hydrate_next();
return;
}
- if (effect !== undefined) {
- destroy_effect(effect);
- effect = undefined;
+ if (effect.nodes_start !== null) {
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
+ effect.nodes_start = effect.nodes_end = null;
}
if (value === '') return;
- effect = branch(() => {
- if (hydrating) {
- // We're deliberately not trying to repair mismatches between server and client,
- // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
- var hash = /** @type {Comment} */ (hydrate_node).data;
- var next = hydrate_next();
- var last = next;
-
- while (
- next !== null &&
- (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
- ) {
- last = next;
- next = /** @type {TemplateNode} */ (get_next_sibling(next));
- }
-
- if (next === null) {
- w.hydration_mismatch();
- throw HYDRATION_ERROR;
- }
-
- if (DEV && !skip_warning) {
- check_hash(/** @type {Element} */ (next.parentNode), hash, value);
- }
-
- assign_nodes(hydrate_node, last);
- anchor = set_hydrate_node(next);
- return;
- }
+ if (hydrating) {
+ // We're deliberately not trying to repair mismatches between server and client,
+ // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
+ var hash = /** @type {Comment} */ (hydrate_node).data;
+ var next = hydrate_next();
+ var last = next;
- var html = value + '';
- if (svg) html = ``;
- else if (mathml) html = ``;
+ while (next !== null && (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')) {
+ last = next;
+ next = /** @type {TemplateNode} */ (get_next_sibling(next));
+ }
- // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
- // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
- /** @type {DocumentFragment | Element} */
- var node = create_fragment_from_html(html);
+ if (next === null) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
- if (svg || mathml) {
- node = /** @type {Element} */ (get_first_child(node));
+ if (DEV && !skip_warning) {
+ check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}
- assign_nodes(
- /** @type {TemplateNode} */ (get_first_child(node)),
- /** @type {TemplateNode} */ (node.lastChild)
- );
-
- if (svg || mathml) {
- while (get_first_child(node)) {
- anchor.before(/** @type {Node} */ (get_first_child(node)));
- }
- } else {
- anchor.before(node);
+ assign_nodes(hydrate_node, last);
+ anchor = set_hydrate_node(next);
+ return;
+ }
+
+ var html = value + '';
+ if (svg) html = ``;
+ else if (mathml) html = ``;
+
+ // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
+ // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
+ /** @type {DocumentFragment | Element} */
+ var node = create_fragment_from_html(html);
+
+ if (svg || mathml) {
+ node = /** @type {Element} */ (get_first_child(node));
+ }
+
+ assign_nodes(
+ /** @type {TemplateNode} */ (get_first_child(node)),
+ /** @type {TemplateNode} */ (node.lastChild)
+ );
+
+ if (svg || mathml) {
+ while (get_first_child(node)) {
+ anchor.before(/** @type {Node} */ (get_first_child(node)));
}
- });
+ } else {
+ anchor.before(node);
+ }
});
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 423c436fe4..925abb9d9d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
hydrate_node,
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index b916a02ce5..c6dce26bfe 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -1,7 +1,7 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
@@ -15,6 +15,7 @@ import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { noop } from '../../../shared/utils.js';
+import { prevent_snippet_stringification } from '../../../shared/validate.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -60,7 +61,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
- return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
+ const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@@ -70,6 +71,10 @@ export function wrap_snippet(component, fn) {
set_dev_current_component_function(previous_component_function);
}
};
+
+ prevent_snippet_stringification(snippet);
+
+ return snippet;
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index 72157eaa40..ad21436505 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -1,5 +1,5 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 18641300e5..43f669e844 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -20,7 +20,7 @@ import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js';
import { DEV } from 'esm-env';
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index e3e3eacad7..db2a0c4ef1 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HEAD_EFFECT } from '../../constants.js';
+import { HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.js';
/**
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 5a5d5d7c9b..f63f55cc6e 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -4,7 +4,7 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
-import { LOADING_ATTR_SYMBOL } from '../../constants.js';
+import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_idle_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import {
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
index 4893426d55..30a8dac1af 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
@@ -1,4 +1,3 @@
-import { hydrating } from '../../hydration.js';
import { render_effect, effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
index 56b0a56e71..e9bbcedc6f 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../../../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js
index ecbfcbc010..fc081b8956 100644
--- a/packages/svelte/src/internal/client/dom/elements/class.js
+++ b/packages/svelte/src/internal/client/dom/elements/class.js
@@ -14,7 +14,11 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
- if (hydrating || prev !== value) {
+ if (
+ hydrating ||
+ prev !== value ||
+ prev === undefined // for edge case of `class={undefined}`
+ ) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js
index 6195b2c561..2d118bfab3 100644
--- a/packages/svelte/src/internal/client/dom/elements/custom-element.js
+++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js
@@ -1,5 +1,5 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
-import { destroy_effect, effect_root, render_effect } from '../../reactivity/effects.js';
+import { effect_root, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index 0c1bb1dada..3374fe713f 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -1,4 +1,3 @@
-/** @import { Location } from 'locate-character' */
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js
index fbc1da95df..cc895cbccb 100644
--- a/packages/svelte/src/internal/client/dom/elements/transitions.js
+++ b/packages/svelte/src/internal/client/dom/elements/transitions.js
@@ -12,7 +12,7 @@ import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
-import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
+import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
diff --git a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
index 918832dfa5..2e5312f1b0 100644
--- a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
+++ b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
@@ -1,4 +1,3 @@
-/** @import { ActionReturn } from 'svelte/action' */
import { noop } from '../../../shared/utils.js';
import { user_pre_effect } from '../../reactivity/effects.js';
import { on } from '../elements/events.js';
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 a5f93e8b17..14d6e29f5b 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';
@@ -164,7 +157,8 @@ export {
invalid_default_snippet,
validate_dynamic_element_tag,
validate_store,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index fab271c916..fd5706eaf2 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,7 +1,6 @@
-/** @import { ProxyMetadata, Source } from '#client' */
+/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
-import { component_context } from './context.js';
import {
array_prototype,
get_descriptor,
@@ -9,24 +8,19 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
-import { check_ownership, widen_ownership } from './dev/ownership.js';
import { state as source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
import { tracing_mode_flag } from '../flags/index.js';
-/** @type {ProxyMetadata | null} */
-var parent_metadata = null;
-
/**
* @template T
* @param {T} value
- * @param {Source} [prev] dev mode only
* @returns {T}
*/
-export function proxy(value, prev) {
+export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
@@ -55,16 +49,7 @@ export function proxy(value, 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;
@@ -76,31 +61,6 @@ export function proxy(value, prev) {
sources.set('length', source(/** @type {any[]} */ (value).length, 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 (
@@ -140,6 +100,7 @@ export function proxy(value, prev) {
prop,
with_parent(() => source(UNINITIALIZED, stack))
);
+ update_version(version);
}
} else {
// When working with arrays, we need to also ensure we update the length when removing
@@ -160,10 +121,6 @@ export function proxy(value, prev) {
},
get(target, prop, receiver) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return metadata;
- }
-
if (prop === STATE_SYMBOL) {
return value;
}
@@ -179,22 +136,6 @@ export function proxy(value, 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;
}
@@ -225,10 +166,6 @@ export function proxy(value, prev) {
},
has(target, prop) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return true;
- }
-
if (prop === STATE_SYMBOL) {
return true;
}
@@ -295,15 +232,6 @@ export function proxy(value, 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);
// Set the new value before updating any signals so that any listeners get the new value
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index cd7bbba02f..21780be862 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -1,6 +1,6 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
-import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
+import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import {
active_reaction,
active_effect,
@@ -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/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 468bb94ab4..36be1ecd04 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -33,7 +33,7 @@ import {
MAYBE_DIRTY,
EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT
-} from '../constants.js';
+} from '#client/constants';
import { set } from './sources.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@@ -427,18 +427,7 @@ export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
- /** @type {TemplateNode | null} */
- var node = effect.nodes_start;
- var end = effect.nodes_end;
-
- while (node !== null) {
- /** @type {TemplateNode | null} */
- var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
-
- node.remove();
- node = next;
- }
-
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
removed = true;
}
@@ -480,6 +469,21 @@ export function destroy_effect(effect, remove_dom = true) {
null;
}
+/**
+ *
+ * @param {TemplateNode | null} node
+ * @param {TemplateNode} end
+ */
+export function remove_effect_dom(node, end) {
+ while (node !== null) {
+ /** @type {TemplateNode | null} */
+ var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
+
+ node.remove();
+ node = next;
+ }
+}
+
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js
index 37a9994ab8..1041238573 100644
--- a/packages/svelte/src/internal/client/reactivity/equality.js
+++ b/packages/svelte/src/internal/client/reactivity/equality.js
@@ -1,4 +1,5 @@
/** @import { Equals } from '#client' */
+
/** @type {Equals} */
export function equals(value) {
return value === this.v;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index bd85b14df0..8bfd8f9e25 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -13,7 +13,7 @@ import { derived, derived_safe_equal } from './deriveds.js';
import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
-import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
+import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index e4834902fe..9d2ad2baee 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -1,4 +1,4 @@
-/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
+/** @import { Derived, Effect, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import {
active_reaction,
@@ -12,7 +12,6 @@ import {
increment_write_version,
update_effect,
reaction_sources,
- set_reaction_sources,
check_dirtiness,
untracking,
is_destroying_effect,
@@ -28,14 +27,14 @@ import {
UNOWNED,
MAYBE_DIRTY,
BLOCK_EFFECT,
- ROOT_EFFECT,
- EFFECT_IS_UPDATING
-} from '../constants.js';
+ ROOT_EFFECT
+} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { proxy } from '../proxy.js';
+import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
export const old_values = new Map();
@@ -78,6 +77,7 @@ export function source(v, stack) {
* @param {V} v
* @param {Error | null} [stack]
*/
+/*#__NO_SIDE_EFFECTS__*/
export function state(v, stack) {
const s = source(v, stack);
@@ -139,7 +139,7 @@ export function set(source, value, should_proxy = false) {
e.state_unsafe_mutation();
}
- let new_value = should_proxy ? proxy(value, source) : value;
+ let new_value = should_proxy ? proxy(value) : value;
return internal_set(source, new_value);
}
@@ -161,7 +161,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');
@@ -171,6 +170,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..7a926bf624 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -94,18 +94,11 @@ export function set_active_effect(effect) {
*/
export let reaction_sources = null;
-/**
- * @param {Source[] | null} sources
- */
-export function set_reaction_sources(sources) {
- reaction_sources = sources;
-}
-
/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) {
- set_reaction_sources([value]);
+ reaction_sources = [value];
} else {
reaction_sources.push(value);
}
@@ -298,31 +291,21 @@ export function handle_error(error, effect, previous_effect, component_context)
is_throwing_error = true;
}
- if (
- !DEV ||
- component_context === null ||
- !(error instanceof Error) ||
- handled_errors.has(error)
- ) {
- propagate_error(error, effect);
- return;
- }
-
- handled_errors.add(error);
+ if (DEV && component_context !== null && error instanceof Error && !handled_errors.has(error)) {
+ handled_errors.add(error);
- const component_stack = [];
+ const component_stack = [];
- const effect_name = effect.fn?.name;
+ const effect_name = effect.fn?.name;
- if (effect_name) {
- component_stack.push(effect_name);
- }
+ if (effect_name) {
+ component_stack.push(effect_name);
+ }
- /** @type {ComponentContext | null} */
- let current_context = component_context;
+ /** @type {ComponentContext | null} */
+ let current_context = component_context;
- while (current_context !== null) {
- if (DEV) {
+ while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.[FILENAME];
@@ -330,35 +313,36 @@ export function handle_error(error, effect, previous_effect, component_context)
const file = filename.split('/').pop();
component_stack.push(file);
}
+
+ current_context = current_context.p;
}
- current_context = current_context.p;
- }
+ const indent = is_firefox ? ' ' : '\t';
+ define_property(error, 'message', {
+ value:
+ error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
+ });
+ define_property(error, 'component_stack', {
+ value: component_stack
+ });
- const indent = is_firefox ? ' ' : '\t';
- define_property(error, 'message', {
- value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
- });
- define_property(error, 'component_stack', {
- value: component_stack
- });
-
- const stack = error.stack;
-
- // Filter out internal files from callstack
- if (stack) {
- const lines = stack.split('\n');
- const new_lines = [];
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- if (line.includes('svelte/src/internal')) {
- continue;
+ const stack = error.stack;
+
+ // Filter out internal files from callstack
+ if (stack) {
+ const lines = stack.split('\n');
+ const new_lines = [];
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (line.includes('svelte/src/internal')) {
+ continue;
+ }
+ new_lines.push(line);
}
- new_lines.push(line);
+ define_property(error, 'stack', {
+ value: new_lines.join('\n')
+ });
}
- define_property(error, 'stack', {
- value: new_lines.join('\n')
- });
}
propagate_error(error, effect);
@@ -476,7 +460,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 !== null && previous_reaction !== reaction) {
read_version++;
if (untracked_writes !== null) {
@@ -692,6 +676,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 +686,6 @@ function flush_queued_root_effects() {
if (DEV) {
dev_effect_stack = [];
}
- old_values.clear();
}
}
@@ -796,19 +780,12 @@ function process_effects(root) {
} else if (is_branch) {
effect.f ^= CLEAN;
} else {
- // Ensure we set the effect to be the active reaction
- // to ensure that unowned deriveds are correctly tracked
- // because we're flushing the current effect
- var previous_active_reaction = active_reaction;
try {
- active_reaction = effect;
if (check_dirtiness(effect)) {
update_effect(effect);
}
} catch (error) {
handle_error(error, effect, null, effect.ctx);
- } finally {
- active_reaction = previous_active_reaction;
}
}
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 0c260a0a9f..b46bdf2013 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..efc761d7c5 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 { HeadPayload, Payload } from './payload.js';
/**
* @typedef {{
@@ -24,14 +26,6 @@ let parent = null;
/** @type {Set} */
let seen;
-/**
- * @param {Element} element
- */
-function stringify(element) {
- if (element.filename === null) return `\`<${element.tag}>\``;
- return `\`<${element.tag}>\` (${element.filename}:${element.line}:${element.column})`;
-}
-
/**
* @param {Payload} payload
* @param {string} message
@@ -98,3 +92,16 @@ 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' ||
+ // for some reason typescript consider the type of payload as never after the first instanceof
+ !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
+ ) {
+ invalid_snippet_arguments();
+ }
+}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 6098b496c5..b58a1d4372 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';
@@ -550,7 +509,8 @@ export { fallback } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { escape_html as escape };
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
new file mode 100644
index 0000000000..8df5787ba4
--- /dev/null
+++ b/packages/svelte/src/internal/server/payload.js
@@ -0,0 +1,72 @@
+export class HeadPayload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+ title = '';
+
+ constructor(css = new Set(), out = '', title = '', uid = () => '') {
+ this.css = css;
+ this.out = out;
+ this.title = title;
+ this.uid = uid;
+ }
+}
+
+export class Payload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+
+ head = new HeadPayload();
+
+ 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 = new HeadPayload();
+ payload.head.out = head.out;
+ payload.head.css = new Set(head.css);
+ payload.head.title = head.title;
+ payload.head.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..b8606fbf6f 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
@@ -33,6 +48,21 @@ export function lifecycle_outside_component(name) {
}
}
+/**
+ * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+ * @returns {never}
+ */
+export function snippet_without_render_tag() {
+ if (DEV) {
+ const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/snippet_without_render_tag`);
+ }
+}
+
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name
diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js
index 852c0e83bf..bbb237594b 100644
--- a/packages/svelte/src/internal/shared/validate.js
+++ b/packages/svelte/src/internal/shared/validate.js
@@ -35,3 +35,15 @@ export function validate_store(store, name) {
e.store_invalid_shape(name);
}
}
+
+/**
+ * @template {() => unknown} T
+ * @param {T} fn
+ */
+export function prevent_snippet_stringification(fn) {
+ fn.toString = () => {
+ e.snippet_without_render_tag();
+ return '';
+ };
+ return fn;
+}
diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js
index 33da2e1761..721673bc36 100644
--- a/packages/svelte/src/reactivity/date.js
+++ b/packages/svelte/src/reactivity/date.js
@@ -5,6 +5,38 @@ import { active_reaction, get, set_active_reaction } from '../internal/client/ru
var inited = false;
+/**
+ * A reactive version of the built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object.
+ * Reading the date (whether with methods like `date.getTime()` or `date.toString()`, or via things like [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat))
+ * in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated when the value of the date changes.
+ *
+ * ```svelte
+ *
+ *
+ *
The time is {formatter.format(date)}
+ * ```
+ */
export class SvelteDate extends Date {
#time = source(super.getTime());
diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js
index 3fa2945ef0..3ae8fe5ad1 100644
--- a/packages/svelte/src/reactivity/map.js
+++ b/packages/svelte/src/reactivity/map.js
@@ -5,6 +5,47 @@ import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
/**
+ * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object.
+ * Reading contents of the map (by iterating, or by reading `map.size` or calling `map.get(...)` or `map.has(...)` as in the [tic-tac-toe example](https://svelte.dev/playground/0b0ff4aa49c9443f9b47fe5203c78293) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the map is updated.
+ *
+ * Note that values in a reactive map are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ *
+ * {#each Array(9), i}
+ *
+ * {/each}
+ *
+ *
+ * {#if winner}
+ *
{winner} wins!
+ *
+ * {:else}
+ *
{player} is next
+ * {/if}
+ * ```
+ *
* @template K
* @template V
* @extends {Map}
diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js
index be0c2d2cf5..4a0b4dfdb3 100644
--- a/packages/svelte/src/reactivity/set.js
+++ b/packages/svelte/src/reactivity/set.js
@@ -10,6 +10,37 @@ var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'un
var inited = false;
/**
+ * A reactive version of the built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) object.
+ * Reading contents of the set (by iterating, or by reading `set.size` or calling `set.has(...)` as in the [example](https://svelte.dev/playground/53438b51194b4882bcc18cddf9f96f15) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the set is updated.
+ *
+ * Note that values in a reactive set are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ * {#each ['🙈', '🙉', '🙊'] as monkey}
+ *
+ * {/each}
+ *
+ *
+ *
+ * {#if monkeys.has('🙈')}
see no evil
{/if}
+ * {#if monkeys.has('🙉')}
hear no evil
{/if}
+ * {#if monkeys.has('🙊')}
speak no evil
{/if}
+ * ```
+ *
* @template T
* @extends {Set}
*/
diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js
index 13f6971996..c1a8275f15 100644
--- a/packages/svelte/src/reactivity/url-search-params.js
+++ b/packages/svelte/src/reactivity/url-search-params.js
@@ -5,6 +5,32 @@ import { increment } from './utils.js';
export const REPLACE = Symbol();
+/**
+ * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
+ * Reading its contents (by iterating, or by calling `params.get(...)` or `params.getAll(...)` as in the [example](https://svelte.dev/playground/b3926c86c5384bab9f2cf993bc08c1c8) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the params are updated.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ *
+ *
+ *
\ 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-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
index 2ffb7e653f..4f75e82aae 100644
--- a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
@@ -36,8 +36,6 @@ export default test({
btn1.click();
});
- console.warn(logs);
-
// the five components guarded by `count < 2` unmount and log
assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index fc748ce6b2..14b6cff841 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -351,7 +351,7 @@ async function run_test_variant(
// @ts-expect-error
globalThis.__svelte.uid = 1;
- if (manual_hydrate) {
+ if (manual_hydrate && variant === 'hydrate') {
hydrate_fn = () => {
instance = hydrate(mod.default, {
target,
@@ -469,10 +469,6 @@ async function run_test_variant(
throw err;
}
} finally {
- console.log = console_log;
- console.warn = console_warn;
- console.error = console_error;
-
config.after_test?.();
// Free up the microtask queue
@@ -486,6 +482,10 @@ async function run_test_variant(
process.on('unhandledRejection', listener);
});
}
+
+ console.log = console_log;
+ console.warn = console_warn;
+ console.error = console_error;
}
}
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();