Merge remote-tracking branch 'upstream/main' into allow-state-return

pull/15589/head
ComputerGuy 5 months ago
commit 610420561c

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow characters in the supplementary special-purpose plane

@ -8,16 +8,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: pnpm/action-setup@v4
- name: install corepack
run: npm i -g corepack@0.31.0
- run: corepack enable
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 22.x
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies

@ -2,7 +2,7 @@
title: Getting started 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 ```bash
npx sv create myapp 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 ## 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. 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.

@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files
Besides `.svelte` files, Svelte also operates on `.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] > [!LEGACY]
> This is a concept that didn't exist prior to Svelte 5 > This is a concept that didn't exist prior to Svelte 5

@ -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
<script>
let { foo, bar, baz } = $props();
// Values that are passed in as props
// are immediately available
console.log({ foo, bar, baz });
</script>
```
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
<script>
let { foo = 'optional default initial value' } = $props();
</script>
```
To get all properties, use rest syntax:
```svelte
<script>
let { a, b, c, ...everythingElse } = $props();
</script>
```
You can use reserved words as prop names.
```svelte
<script>
// creates a `class` property, even
// though it is a reserved word
let { class: className } = $props();
</script>
```
If you're using TypeScript, you can declare the prop types:
```svelte
<script lang="ts">
interface Props {
required: string;
optional?: number;
[key: string]: unknown;
}
let { required, optional, ...everythingElse }: Props = $props();
</script>
```
If you're using JavaScript, you can declare the prop types using JSDoc:
```svelte
<script>
/** @type {{ x: string }} */
let { x } = $props();
// or use @typedef if you want to document the properties:
/**
* @typedef {Object} MyProps
* @property {string} y Some documentation
*/
/** @type {MyProps} */
let { y } = $props();
</script>
```
If you export a `const`, `class` or `function`, it is readonly from outside the component.
```svelte
<script>
export const thisIs = 'readonly';
export function greet(name) {
alert(`hello ${name}!`);
}
</script>
```
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
<script>
let count = $state(0);
function handleClick() {
// calling this function will trigger an
// update if the markup references `count`
count = count + 1;
}
</script>
```
Svelte's `<script>` blocks are run only when the component is created, so assignments within a `<script>` block are not automatically run again when a prop updates.
```svelte
<script>
let { person } = $props();
// this will only set `name` on component creation
// it will not update when `person` does
let { name } = person;
</script>
```
If you'd like to react to changes to a prop, use the `$derived` or `$effect` runes instead.
```svelte
<script>
let count = $state(0);
let double = $derived(count * 2);
$effect(() => {
if (count > 10) {
alert('Too high!');
}
});
</script>
```
For more information on reactivity, read the documentation around runes.

@ -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
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
```
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
> <script>
> let count = 0;
> </script>
>
> <button on:click={() => count++}>
> clicks: {count}
> </button>
> ```
## `$derived`
Derived state is declared with the `$derived` rune:
```svelte
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<button onclick={() => count++}>
{doubled}
</button>
<p>{count} doubled is {doubled}</p>
```
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
> <script>
> let count = 0;
> $: doubled = count * 2;
> </script>
>
> <button on:click={() => count++}>
> {doubled}
> </button>
>
> <p>{count} doubled is {doubled}</p>
> ```
>
> 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
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// this will re-run whenever `color` or `size` change
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
```
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
> <script>
> let size = 50;
> let color = '#ff3e00';
>
> let canvas;
>
> $: {
> const context = canvas.getContext('2d');
> context.clearRect(0, 0, canvas.width, canvas.height);
>
> // this will re-run whenever `color` or `size` change
> context.fillStyle = color;
> context.fillRect(0, 0, size, size);
> }
> </script>
>
> <canvas bind:this={canvas} width="100" height="100" />
> ```
>
> This only worked at the top level of a component.

@ -250,3 +250,83 @@ console.log(total.value); // 7
``` ```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead. ...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<T> {
value: T;
}
interface Svelte {
state<T>(value?: T): Signal<T>;
get<T>(source: Signal<T>): T;
set<T>(source: Signal<T>, 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;
}
```

@ -25,7 +25,7 @@ You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4
}); });
</script> </script>
<canvas bind:this={canvas} width="100" height="100" /> <canvas bind:this={canvas} width="100" height="100"></canvas>
``` ```
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. 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. 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 ```ts
let a = false; // @filename: ambient.d.ts
let b = false; declare module 'canvas-confetti' {
interface ConfettiOptions {
colors: string[];
}
function confetti(opts?: ConfettiOptions): void;
export default confetti;
}
// @filename: index.js
// ---cut--- // ---cut---
$effect(() => { import confetti from 'canvas-confetti';
console.log('running');
if (a) { let condition = $state(true);
console.log('b:', b); 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. 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 ```js
<script> const destroy = $effect.root(() => {
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => { $effect(() => {
console.log(count); // setup
}); });
return () => { return () => {
console.log('effect root cleanup'); // cleanup
}; };
}); });
</script>
// later...
destroy();
``` ```
## When not to use `$effect` ## When not to use `$effect`

@ -37,7 +37,7 @@ On the other side, inside `MyComponent.svelte`, we can receive props with the `$
## Fallback values ## 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 ```js
let { adjective = 'happy' } = $props(); let { adjective = 'happy' } = $props();

@ -2,7 +2,7 @@
title: $host 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)):
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte

@ -154,6 +154,8 @@ A JavaScript expression can be included as text by surrounding it with curly bra
{expression} {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: `&lbrace;`, `&lcub;`, or `&#123;` for `{` and `&rbrace;`, `&rcub;`, or `&#125;` for `}`. Curly braces can be included in a Svelte template by using their [HTML entity](https://developer.mozilla.org/docs/Glossary/Entity) strings: `&lbrace;`, `&lcub;`, or `&#123;` for `{` and `&rbrace;`, `&rcub;`, or `&#125;` 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. 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.

@ -112,6 +112,8 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
## Passing snippets to components ## 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)): 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 ```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. 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)): 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 ```svelte
@ -165,6 +169,8 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table> </Table>
``` ```
### 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)): 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 ```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 > [!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... 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 ```svelte

@ -22,10 +22,6 @@ The `transition:` directive indicates a _bidirectional_ transition, which means
{/if} {/if}
``` ```
## Built-in transitions
A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
## Local vs global ## 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. 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} {/if}
``` ```
## Built-in transitions
A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
## Transition parameters ## Transition parameters
Transitions can have parameters. Transitions can have parameters.

@ -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
<!--- copy: false --->
{#each expression as name}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name, index}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name (key)}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name, index (key)}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name}...{:else}...{/each}
```
Iterating over lists of values can be done with an each block.
```svelte
<h1>Shopping list</h1>
<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>
```
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}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/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)}
<li>{item.name} x {item.qty}</li>
{/each}
<!-- or with additional index value -->
{#each items as item, i (item.id)}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/each}
```
You can freely use destructuring and rest patterns in each blocks.
```svelte
{#each items as { id, name, qty }, i (id)}
<li>{i + 1}: {name} x {qty}</li>
{/each}
{#each objects as { id, ...rest }}
<li><span>{id}</span><MyComponent {...rest} /></li>
{/each}
{#each items as [id, ...rest]}
<li><span>{id}</span><MyComponent values={rest} /></li>
{/each}
```
An each block can also have an `{:else}` clause, which is rendered if the list is empty.
```svelte
{#each todos as todo}
<p>{todo.text}</p>
{:else}
<p>No tasks today!</p>
{/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.

@ -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
<script>
let response = $state();
fetch('/api/data').then(async (r) => (response = r.json()));
</script>
```
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).

@ -94,7 +94,7 @@ interface User {}
// ---cut--- // ---cut---
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from 'svelte';
let key = {}; const key = {};
/** @param {User} user */ /** @param {User} user */
export function setUserContext(user) { export function setUserContext(user) {

@ -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 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. [Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.

@ -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

@ -21,7 +21,7 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
### component_api_changed ### 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. 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 `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: This is forbidden because it introduces instability: if `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
```js ```js
let count = 0;
// ---cut---
let even = $derived(count % 2 === 0); let even = $derived(count % 2 === 0);
let odd = $derived(!even); let odd = $derived(!even);
``` ```

@ -161,7 +161,7 @@ Tried to unmount a component that was not mounted
### ownership_invalid_binding ### 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 `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` inside `Parent`, this warning is thrown. Consider three components `GrandParent`, `Parent` and `Child`. If you do `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` 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 ### 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 Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using 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
``` ```
Consider the following code: Consider the following code:

@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations
### css_global_block_invalid_list ### 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 ### 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 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 ### css_global_invalid_placement
``` ```

@ -823,15 +823,16 @@ See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more
### state_referenced_locally ### 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: This warning is thrown when the compiler detects the following:
- A reactive variable is declared - A reactive variable is declared
- the variable is reassigned - ...and later reassigned...
- the variable is referenced inside the same scope it is declared and it is a non-reactive context - ...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 ```svelte
<!--- file: Parent.svelte ---> <!--- file: Parent.svelte --->

@ -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. 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 ### lifecycle_outside_component
``` ```
@ -54,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button> <button onclick={handleClick}>click me</button>
``` ```
### 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
<script>
let { children } = $props();
</script>
{children}
```
...or like this (a parent component is passing a snippet where a non-snippet value is expected):
```svelte
<!--- file: Parent.svelte --->
<ChildComponent>
{#snippet label()}
<span>Hi!</span>
{/snippet}
</ChildComponent>
```
```svelte
<!--- file: Child.svelte --->
<script>
let { label } = $props();
</script>
<!-- This component doesn't expect a snippet, but the parent provided one -->
<p>{label}</p>
```
### store_invalid_shape ### store_invalid_shape
``` ```

@ -2,24 +2,6 @@
title: svelte/reactivity 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 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.
```svelte
<script>
import { SvelteURL } from 'svelte/reactivity';
const url = new SvelteURL('https://example.com/path');
</script>
<!-- changes to these... -->
<input bind:value={url.protocol} />
<input bind:value={url.hostname} />
<input bind:value={url.pathname} />
<hr />
<!-- will update `href` and vice versa -->
<input bind:value={url.href} />
```
> MODULE: svelte/reactivity > MODULE: svelte/reactivity

@ -5,7 +5,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@9.4.0", "packageManager": "pnpm@10.4.0",
"engines": { "engines": {
"pnpm": ">=9.0.0" "pnpm": ">=9.0.0"
}, },

@ -1,5 +1,163 @@
# svelte # 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 `<svelte:boundary>` 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 ## 5.25.3
### Patch Changes ### Patch Changes

@ -12,7 +12,7 @@
## component_api_changed ## 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. 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 `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: This is forbidden because it introduces instability: if `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
```js ```js
let count = 0;
// ---cut---
let even = $derived(count % 2 === 0); let even = $derived(count % 2 === 0);
let odd = $derived(!even); let odd = $derived(!even);
``` ```

@ -132,7 +132,7 @@ During development, this error is often preceeded by a `console.error` detailing
## ownership_invalid_binding ## 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 `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` inside `Parent`, this warning is thrown. Consider three components `GrandParent`, `Parent` and `Child`. If you do `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` 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 ## 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 > Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using 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
Consider the following code: Consider the following code:

@ -16,7 +16,31 @@
## css_global_block_invalid_list ## 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 ## css_global_block_invalid_modifier
@ -26,6 +50,10 @@
> A `:global` selector can only be modified if it is a descendant of other selectors > 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 ## css_global_invalid_placement
> `:global(...)` can be at the start or end of a selector sequence, but not in the middle > `:global(...)` can be at the start or end of a selector sequence, but not in the middle

@ -54,14 +54,15 @@ To fix this, wrap your variable declaration with `$state`.
## state_referenced_locally ## 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: This warning is thrown when the compiler detects the following:
- A reactive variable is declared - A reactive variable is declared
- the variable is reassigned - ...and later reassigned...
- the variable is referenced inside the same scope it is declared and it is a non-reactive context - ...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 ```svelte
<!--- file: Parent.svelte ---> <!--- file: Parent.svelte --->

@ -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. 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 ## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation > `%name%(...)` can only be used during component initialisation
@ -48,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button> <button onclick={handleClick}>click me</button>
``` ```
## 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
<script>
let { children } = $props();
</script>
{children}
```
...or like this (a parent component is passing a snippet where a non-snippet value is expected):
```svelte
<!--- file: Parent.svelte --->
<ChildComponent>
{#snippet label()}
<span>Hi!</span>
{/snippet}
</ChildComponent>
```
```svelte
<!--- file: Child.svelte --->
<script>
let { label } = $props();
</script>
<!-- This component doesn't expect a snippet, but the parent provided one -->
<p>{label}</p>
```
## store_invalid_shape ## store_invalid_shape
> `%name%` is not a store with a `subscribe` method > `%name%` is not a store with a `subscribe` method

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.25.3", "version": "5.28.2",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -103,6 +103,17 @@
"default": "./src/events/index.js" "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": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/sveltejs/svelte.git", "url": "git+https://github.com/sveltejs/svelte.git",
@ -156,7 +167,7 @@
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^1.4.3", "esrap": "^1.4.6",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",

@ -24,7 +24,12 @@ await createBundle({
output: `${dir}/types/index.d.ts`, output: `${dir}/types/index.d.ts`,
compilerOptions: { compilerOptions: {
// so that types/properties with `@internal` (and its dependencies) are removed from the output // 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: { modules: {
[pkg.name]: `${dir}/src/index.d.ts`, [pkg.name]: `${dir}/src/index.d.ts`,

@ -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 * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_list(node) { 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`); 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 * `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -1,7 +1,7 @@
/** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */ /** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */ /** @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 { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -944,21 +944,19 @@ const instance_script = {
node.body.type === 'ExpressionStatement' && node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression' node.body.expression.type === 'AssignmentExpression'
) { ) {
const ids = extract_identifiers(node.body.expression.left); const { left, right } = node.body.expression;
const [, expression_ids] = extract_all_identifiers_from_expression(
node.body.expression.right const ids = extract_identifiers(left);
); const [, expression_ids] = extract_all_identifiers_from_expression(right);
const bindings = ids.map((id) => state.scope.get(id.name)); const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name)));
const reassigned_bindings = bindings.filter((b) => b?.reassigned);
if (bindings.every((b) => b.kind === 'legacy_reactive')) {
if ( if (
node.body.expression.right.type !== 'Literal' && right.type !== 'Literal' &&
!bindings.some((b) => b?.kind === 'store_sub') && bindings.every((b) => b.kind !== 'store_sub') &&
node.body.expression.left.type !== 'MemberExpression' left.type !== 'MemberExpression'
) { ) {
let { start, end } = /** @type {{ start: number, end: number }} */ ( let { start, end } = /** @type {{ start: number, end: number }} */ (right);
node.body.expression.right
);
check_rune_binding('derived'); check_rune_binding('derived');
@ -969,7 +967,7 @@ const instance_script = {
'let ' 'let '
); );
if (node.body.expression.right.type === 'SequenceExpression') { if (right.type === 'SequenceExpression') {
while (state.str.original[start] !== '(') start -= 1; while (state.str.original[start] !== '(') start -= 1;
while (state.str.original[end - 1] !== ')') end += 1; while (state.str.original[end - 1] !== ')') end += 1;
} }
@ -983,15 +981,16 @@ const instance_script = {
} }
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'); check_rune_binding('state');
const init = const init =
binding.kind === 'state' binding.kind === 'state'
? ' = $state()' ? ' = $state()'
: expression_ids.length === 0 : 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 // implicitly-declared variable which we need to make explicit
state.str.prependLeft( 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)); state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return; return;
} }

@ -118,6 +118,7 @@ function read_rule(parser) {
metadata: { metadata: {
parent_rule: null, parent_rule: null,
has_local_selectors: false, has_local_selectors: false,
has_global_selectors: false,
is_global_block: false is_global_block: false
} }
}; };
@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) {
children, children,
metadata: { metadata: {
rule: null, rule: null,
is_global: false,
used: false used: false
} }
}; };

@ -1,7 +1,7 @@
/** @import { Context, Visitors } from 'zimmerframe' */ /** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */ /** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as b from '../../utils/builders.js'; import * as b from '#compiler/builders';
import * as e from '../../errors.js'; 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 // until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation; delete n.typeAnnotation;
delete n.typeParameters; delete n.typeParameters;
delete n.typeArguments;
delete n.returnType; delete n.returnType;
delete n.accessibility; delete n.accessibility;
}, },
@ -94,6 +95,9 @@ const visitors = {
TSTypeAliasDeclaration() { TSTypeAliasDeclaration() {
return b.empty; return b.empty;
}, },
TSTypeAssertion(node, context) {
return context.visit(node.expression);
},
TSEnumDeclaration(node) { TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums'); e.typescript_invalid_feature(node, 'enums');
}, },

@ -72,6 +72,8 @@ const NUL = 0;
// to replace them ourselves // to replace them ourselves
// //
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters // 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 */ /** @param {number} code */
function validate_code(code) { function validate_code(code) {
@ -116,5 +118,10 @@ function validate_code(code) {
return 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; return NUL;
} }

@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js'; import { is_global, is_unscoped_pseudo_class } from './utils.js';
/** /**
* @typedef {Visitors< * @typedef {{
* AST.CSS.Node,
* {
* keyframes: string[]; * keyframes: string[];
* rule: AST.CSS.Rule | null; * rule: AST.CSS.Rule | null;
* } * analysis: ComponentAnalysis;
* >} CssVisitors * }} CssState
*/
/**
* @typedef {Visitors<AST.CSS.Node, CssState>} 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<AST.CSS.Node>} path * @param {Array<AST.CSS.Node>} path
@ -42,6 +53,9 @@ const css_visitors = {
if (is_keyframes_node(node)) { if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) { if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude); 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); const global = node.children.find(is_global);
if (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) { 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) // 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++) { 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.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 ({ metadata }) => metadata.is_global || metadata.is_global_like
); );
node.metadata.used ||= node.metadata.is_global;
if ( if (
node.metadata.rule?.metadata.parent_rule && node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector' node.children[0]?.selectors[0]?.type === 'NestingSelector'
@ -177,10 +197,12 @@ const css_visitors = {
Rule(node, context) { Rule(node, context) {
node.metadata.parent_rule = context.state.rule; 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; 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); const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) { if (is_global_block) {
@ -188,70 +210,79 @@ const css_visitors = {
child.metadata.is_global_like = true; child.metadata.is_global_like = true;
} }
if (idx !== -1) { if (idx === 0) {
is_global_block = true; if (
for (let i = idx + 1; i < child.selectors.length; i++) { 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, { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) { ComplexSelector(node) {
node.metadata.used = true; node.metadata.used = true;
} }
}); });
} }
}
if (child.combinator && child.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(child, child.combinator.name);
} }
return is_global_block; 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`
if (node.metadata.is_global_block) { if (is_lone_global && node.prelude.children.length > 1) {
if (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); e.css_global_block_invalid_list(node.prelude);
} }
const complex_selector = node.prelude.children[0]; if (
const global_selector = complex_selector.children.find((r, selector_idx) => { declaration &&
const idx = r.selectors.findIndex(is_global_block_selector); // :global { color: red; } is invalid, but foo :global { color: red; } is valid
if (idx === 0) { node.prelude.children.length === 1 &&
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) { is_lone_global
e.css_global_block_invalid_modifier_start(r.selectors[1]); ) {
e.css_global_block_invalid_declaration(declaration);
}
} }
return true;
} else if (idx !== -1) { } 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 !== ' ') { if (node.metadata.is_global_block && !is_global_block) {
e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name); 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 ( // visit selector list first, to populate child selector metadata
declaration && context.visit(node.prelude, state);
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 && for (const selector of node.prelude.children) {
node.prelude.children[0].children.length === 1 && node.metadata.has_global_selectors ||= selector.metadata.is_global;
node.prelude.children[0].children[0].selectors.length === 1 node.metadata.has_local_selectors ||= !selector.metadata.is_global;
) {
e.css_global_block_invalid_declaration(declaration);
}
} }
context.next({ // if this rule has a ComplexSelector whose RelativeSelector children are all
...context.state, // `:global(...)`, and the rule contains declarations (rather than just
rule: node // 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) => { // visit block list, so parent rule metadata is populated
return selector.children.some( context.visit(node.block, state);
({ metadata }) => !metadata.is_global && !metadata.is_global_like
);
});
}, },
NestingSelector(node, context) { NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule); const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@ -289,5 +320,12 @@ const css_visitors = {
* @param {ComponentAnalysis} analysis * @param {ComponentAnalysis} analysis
*/ */
export function analyze_css(stylesheet, 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);
} }

@ -1,6 +1,11 @@
/** @import * as Compiler from '#compiler' */ /** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe'; 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 { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.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; let sibling_matched = false;
for (const possible_sibling of siblings.keys()) { 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()}<p>foo</p>` with `:global(.x) + p` is a match // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) { if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true; sibling_matched = true;
@ -282,20 +291,26 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
* a global selector * a global selector
* @param {Compiler.AST.CSS.RelativeSelector} selector * @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @returns {boolean}
*/ */
function is_global(selector, rule) { function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) { if (selector.metadata.is_global || selector.metadata.is_global_like) {
return true; return true;
} }
let explicitly_global = false;
for (const s of selector.selectors) { for (const s of selector.selectors) {
/** @type {Compiler.AST.CSS.SelectorList | null} */ /** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null; let selector_list = null;
let can_be_global = false;
let owner = rule; let owner = rule;
if (s.type === 'PseudoClassSelector') { if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) { if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = 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; 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) => return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner) is_global(relative_selector, owner)
); );
}); });
explicitly_global ||= has_global_selectors;
if (!has_global_selectors) { if (!has_global_selectors && !can_be_global) {
return false; return false;
} }
} }
return true; return explicitly_global || selector.selectors.length === 0;
} }
const regex_backslash_and_following_character = /\\(.)/g; const regex_backslash_and_following_character = /\\(.)/g;
@ -814,10 +830,10 @@ function get_element_parent(node) {
* @param {Direction} direction * @param {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen * @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.Component, NodeExistsValue>}
*/ */
function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) { function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.Component, NodeExistsValue>} */
const result = new Map(); const result = new Map();
const path = node.metadata.path; 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, // 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 // so we want to continue until we find a definite sibling even with the adjacent-only combinator
} else if (is_block(node)) { } else if (is_block(node) || node.type === 'Component') {
if (node.type === 'SlotElement') { if (node.type === 'SlotElement' || node.type === 'Component') {
result.set(node, NODE_PROBABLY_EXISTS); result.set(node, NODE_PROBABLY_EXISTS);
} }
const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only); const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result); 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; return result;
} }
} else if (node.type === 'SvelteElement') { } 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 {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen * @param {Set<Compiler.AST.SnippetBlock>} seen
@ -942,6 +962,10 @@ function get_possible_nested_siblings(node, direction, adjacent_only, seen = new
seen.add(node); seen.add(node);
fragments.push(node.body); fragments.push(node.body);
break; break;
case 'Component':
fragments.push(node.fragment, ...[...node.metadata.snippets].map((s) => s.body));
break;
} }
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */

@ -5,8 +5,8 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { extract_identifiers, is_text_attribute } from '../../utils/ast.js'; import { extract_identifiers } from '../../utils/ast.js';
import * as b from '../../utils/builders.js'; import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; 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 check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.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_component_bindings: false,
uses_render_tags: false, uses_render_tags: false,
needs_context: false, needs_context: false,
needs_mutation_validation: false,
needs_props: false, needs_props: false,
event_directive_node: null, event_directive_node: null,
uses_event_attributes: false, uses_event_attributes: false,
@ -455,7 +456,8 @@ export function analyze_component(root, source, options) {
hash hash
}) })
: '', : '',
keyframes: [] keyframes: [],
has_global: false
}, },
source, source,
undefined_exports: new Map(), undefined_exports: new Map(),

@ -3,10 +3,10 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { get_rune } from '../../scope.js'; import { get_rune } from '../../scope.js';
import * as e from '../../../errors.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 { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.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 * @param {CallExpression} node

@ -1,7 +1,5 @@
/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */ /** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js'; import { extract_identifiers } from '../../../utils/ast.js';

@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */ /** @import { Expression, Identifier } from 'estree' */
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js'; 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 * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
/** /**
* @param {Identifier} node * @param {Identifier} node
@ -111,7 +111,34 @@ export function Identifier(node, context) {
(parent.type !== 'AssignmentExpression' || parent.left !== node) && (parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression' 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 ( if (

@ -1,10 +1,7 @@
/** @import { MemberExpression, Node } from 'estree' */ /** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; 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 { is_pure, is_safe_identifier } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {MemberExpression} node * @param {MemberExpression} node

@ -173,7 +173,8 @@ export function RegularElement(node, context) {
if ( if (
context.state.analysis.source[node.end - 2] === '/' && context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) && !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); w.element_invalid_self_closing_tag(node, node.name);
} }

@ -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 { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */ /** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -6,7 +6,7 @@
import * as e from '../../../../errors.js'; import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js'; import { extract_identifiers } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.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'; import { get_rune } from '../../../scope.js';
/** /**

@ -3,7 +3,7 @@
/** @import { ComponentAnalysis, Analysis } from '../../types' */ /** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */ /** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe'; 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 { build_getter, is_state_source } from './utils.js';
import { render_stylesheet } from '../css/index.js'; import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.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 = const should_inject_context =
dev || dev ||
analysis.needs_context || 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)) 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) { if (!analysis.runes) {

@ -1,10 +1,10 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ /** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js'; import * as b from '#compiler/builders';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { is_simple_expression } from '../../../utils/ast.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -13,7 +13,8 @@ import {
PROPS_IS_BINDABLE PROPS_IS_BINDABLE
} from '../../../../constants.js'; } from '../../../../constants.js';
import { dev } from '../../../state.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 * @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; return params;
} }

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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'; import { parse_directive_name } from './shared/utils.js';
/** /**

@ -1,15 +1,16 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
import { import {
build_assignment_value, build_assignment_value,
get_attribute_expression, get_attribute_expression,
is_event_attribute is_event_attribute
} from '../../../../utils/ast.js'; } 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 { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
/** /**
* @param {AssignmentExpression} node * @param {AssignmentExpression} node
@ -20,9 +21,7 @@ export function AssignmentExpression(node, context) {
visit_assignment_expression(node, context, build_assignment) ?? context.next() visit_assignment_expression(node, context, build_assignment) ?? context.next()
); );
return is_ignored(node, 'ownership_invalid_mutation') return validate_mutation(node, context, expression);
? b.call('$.skip_ownership_validation', b.thunk(expression))
: expression;
} }
/** /**
@ -165,7 +164,9 @@ function build_assignment(operator, left, right, context) {
path.at(-1) === 'SvelteComponent' || path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' && (path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' && 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; should_transform = false;
} }

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.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 { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';

@ -1,7 +1,7 @@
/** @import { Expression, BinaryExpression } from 'estree' */ /** @import { Expression, BinaryExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {BinaryExpression} node * @param {BinaryExpression} node

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.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 { binding_properties } from '../../../bindings.js';
import { build_attribute_value } from './shared/element.js'; import { build_attribute_value } from './shared/element.js';
import { build_bind_this, validate_binding } from './shared/utils.js'; import { build_bind_this, validate_binding } from './shared/utils.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 { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {BlockStatement} node * @param {BlockStatement} node

@ -1,6 +1,6 @@
/** @import { BreakStatement } from 'estree' */ /** @import { BreakStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {BreakStatement} node * @param {BreakStatement} node

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression } from 'estree' */ /** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; 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 { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js'; import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js'; import { should_proxy } from '../utils.js';

@ -1,8 +1,6 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */ /** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
/** @import { } from '#compiler' */
/** @import { Context, StateField } from '../types' */ /** @import { Context, StateField } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js'; import { should_proxy } from '../utils.js';
@ -142,31 +140,18 @@ export function ClassBody(node, context) {
// get foo() { return this.#foo; } // get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); 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; } // set foo(value) { this.#foo = value; }
const value = b.id('value'); const val = b.id('value');
body.push( body.push(
b.method( b.method(
'set', 'set',
definition.key, definition.key,
[value], [val],
[b.stmt(b.call('$.set', member, value, field.kind === 'state' && b.true))] [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
) )
); );
} }
if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
continue; continue;
} }
} }
@ -174,33 +159,6 @@ export function ClassBody(node, context) {
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state))); 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 }; return { ...node, body };
} }

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
import { build_component } from './shared/component.js'; import { build_component } from './shared/component.js';
/** /**

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.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 { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';

@ -1,7 +1,7 @@
/** @import { Expression} from 'estree' */ /** @import { Expression} from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.DebugTag} node * @param {AST.DebugTag} node

@ -11,7 +11,7 @@ import {
} from '../../../../../constants.js'; } from '../../../../../constants.js';
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.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 { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';

@ -1,6 +1,6 @@
/** @import { ExportNamedDeclaration } from 'estree' */ /** @import { ExportNamedDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {ExportNamedDeclaration} node * @param {ExportNamedDeclaration} node

@ -1,6 +1,6 @@
/** @import { Expression, ExpressionStatement } from 'estree' */ /** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
/** /**

@ -4,7 +4,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.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 { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { clean_nodes, infer_namespace } from '../../utils.js'; import { clean_nodes, infer_namespace } from '../../utils.js';
import { process_children } from './shared/fragment.js'; import { process_children } from './shared/fragment.js';

@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */ /** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js'; import { build_hoisted_params } from '../utils.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {FunctionDeclaration} node * @param {FunctionDeclaration} node

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js'; import { is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.HtmlTag} node * @param {AST.HtmlTag} node
@ -11,17 +11,22 @@ import * as b from '../../../../utils/builders.js';
export function HtmlTag(node, context) { export function HtmlTag(node, context) {
context.state.template.push('<!>'); context.state.template.push('<!>');
// push into init, so that bindings run afterwards, which might trigger another run and override hydration const expression = /** @type {Expression} */ (context.visit(node.expression));
context.state.init.push(
b.stmt( const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
const statement = b.stmt(
b.call( b.call(
'$.html', '$.html',
context.state.node, context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))), b.thunk(expression),
b.literal(context.state.metadata.namespace === 'svg'), is_svg && b.true,
b.literal(context.state.metadata.namespace === 'mathml'), is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && 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);
} }

@ -1,7 +1,7 @@
/** @import { Identifier, Node } from 'estree' */ /** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import is_reference from 'is-reference'; 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'; import { build_getter } from '../utils.js';
/** /**

@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.IfBlock} node * @param {AST.IfBlock} node

@ -1,6 +1,6 @@
/** @import { ImportDeclaration } from 'estree' */ /** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {ImportDeclaration} node * @param {ImportDeclaration} node

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.KeyBlock} node * @param {AST.KeyBlock} node

@ -1,9 +1,7 @@
/** @import { Location } from 'locate-character' */
/** @import { Expression, LabeledStatement, Statement } from 'estree' */ /** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */ /** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locator } from '../../../../state.js'; import * as b from '#compiler/builders';
import * as b from '../../../../utils/builders.js';
import { build_getter } from '../utils.js'; import { build_getter } from '../utils.js';
/** /**

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
/** /**

@ -1,6 +1,6 @@
/** @import { MemberExpression } from 'estree' */ /** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {MemberExpression} node * @param {MemberExpression} node

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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'; import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [ const modifiers = [

@ -1,7 +1,7 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */ /** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js'; 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'; import { add_state_transformers } from './shared/declarations.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 { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */ /** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -13,7 +13,7 @@ import {
import { escape_html } from '../../../../../escaping.js'; import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js'; import { dev, is_ignored, locator } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.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 { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } 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 : value
); );
const evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment( const inner_assignment = b.assignment(
'=', '=',
b.member(node_id, 'value'), b.member(node_id, 'value'),
b.conditional( evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
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
)
); );
const update = b.stmt( const update = b.stmt(

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js'; import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.RenderTag} node * @param {AST.RenderTag} node

@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */ /** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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 { build_attribute_value } from './shared/element.js';
import { memoize_expression } from './shared/utils.js'; import { memoize_expression } from './shared/utils.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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.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'; import { get_value } from './shared/declarations.js';
/** /**
@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js';
*/ */
export function SnippetBlock(node, context) { export function SnippetBlock(node, context) {
// TODO hoist where possible // TODO hoist where possible
/** @type {Pattern[]} */ /** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')]; const args = [b.id('$$anchor')];
/** @type {BlockStatement} */ /** @type {BlockStatement} */
@ -21,6 +21,10 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const declarations = []; const declarations = [];
if (dev) {
declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
}
const transform = { ...context.state.transform }; const transform = { ...context.state.transform };
const child_state = { ...context.state, transform }; const child_state = { ...context.state, transform };
@ -30,12 +34,7 @@ export function SnippetBlock(node, context) {
if (!argument) continue; if (!argument) continue;
if (argument.type === 'Identifier') { if (argument.type === 'Identifier') {
args.push({ args.push(b.assignment_pattern(argument, b.id('$.noop')));
type: 'AssignmentPattern',
left: argument,
right: b.id('$.noop')
});
transform[argument.name] = { read: b.call }; transform[argument.name] = { read: b.call };
continue; continue;
@ -72,12 +71,10 @@ export function SnippetBlock(node, context) {
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
]); ]);
/** @type {Expression} */ // in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = b.arrow(args, body); let snippet = dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
if (dev) { : b.arrow(args, body);
snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
}
const declaration = b.const(node.expression, snippet); const declaration = b.const(node.expression, snippet);

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.SvelteBoundary} node * @param {AST.SvelteBoundary} node

@ -3,10 +3,10 @@
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, locator } from '../../../../state.js'; import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.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 { determine_namespace_for_children } from '../../utils.js';
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.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 * @param {AST.SvelteElement} node

@ -1,7 +1,7 @@
/** @import { BlockStatement } from 'estree' */ /** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.SvelteHead} node * @param {AST.SvelteHead} node

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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'; import { build_template_chunk } from './shared/utils.js';
/** /**

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../../constants.js'; 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'; import { parse_directive_name } from './shared/utils.js';
/** /**

@ -1,8 +1,8 @@
/** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */ /** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.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 * @param {UpdateExpression} node
@ -51,7 +51,5 @@ export function UpdateExpression(node, context) {
); );
} }
return is_ignored(node, 'ownership_invalid_mutation') return validate_mutation(node, context, update);
? b.call('$.skip_ownership_validation', b.thunk(update))
: update;
} }

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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'; import { parse_directive_name } from './shared/utils.js';
/** /**

@ -3,7 +3,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.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 * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types.js' */ /** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.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_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js'; import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js'; import { build_event_handler } from './events.js';
@ -179,20 +179,30 @@ export function build_component(node, component_name, context, anchor = context.
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression)); const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && attribute.name !== 'this') { 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( binding_initializers.push(
b.stmt( b.stmt(
b.call( b.call(
b.id('$.add_owner_effect'), '$$ownership_validator.binding',
expression.type === 'SequenceExpression' b.literal(binding.node.name),
? expression.expressions[0]
: b.thunk(expression),
b.id(component_name), b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true b.thunk(expression)
) )
) )
); );
} }
}
if (expression.type === 'SequenceExpression') { if (expression.type === 'SequenceExpression') {
if (attribute.name === 'this') { if (attribute.name === 'this') {

@ -1,7 +1,7 @@
/** @import { Identifier } from 'estree' */ /** @import { Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */ /** @import { ComponentContext, Context } from '../../types' */
import { is_state_source } from '../../utils.js'; 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)` * Turns `foo` into `$.get(foo)`

@ -1,11 +1,11 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js'; import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js'; import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.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_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js'; import { build_template_chunk, get_expression_id } from './utils.js';

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js'; import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js'; import { dev, locator } from '../../../../../state.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* @param {AST.Attribute} node * @param {AST.Attribute} node

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { cannot_be_set_statically } from '../../../../../../utils.js'; import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.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 { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js'; import { build_template_chunk } from './utils.js';

@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import * as b from '../../../../../utils/builders.js'; import * as b from '#compiler/builders';
/** /**
* *

@ -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 { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */ /** @import { ComponentClientTransformState, Context } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js'; 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 { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js'; import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference'; 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'; import { create_derived } from '../../utils.js';
/** /**
@ -69,11 +69,17 @@ export function build_template_chunk(
node.metadata.expression 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 (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing // 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). // 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 }; return { value, has_state };
} }
@ -89,14 +95,11 @@ export function build_template_chunk(
} }
} }
const is_defined = if (evaluated.is_known) {
value.type === 'BinaryExpression' || quasi.value.cooked += evaluated.value + '';
(value.type === 'UnaryExpression' && value.operator !== 'void') || } else {
(value.type === 'LogicalExpression' && value.right.type === 'Literal') || if (!evaluated.is_defined) {
(value.type === 'Identifier' && value.name === state.analysis.props_id?.name); // add `?? ''` where necessary
if (!is_defined) {
// add `?? ''` where necessary (TODO optimise more cases)
value = b.logical('??', value, b.literal('')); value = b.logical('??', value, b.literal(''));
} }
@ -106,6 +109,7 @@ export function build_template_chunk(
quasis.push(quasi); quasis.push(quasi);
} }
} }
}
for (const quasi of quasis) { for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
@ -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<Identifier | Literal>} */
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)
);
}

@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) {
// generateMap takes care of calculating source relative to file // generateMap takes care of calculating source relative to file
source: options.filename, source: options.filename,
file: options.cssOutputFilename || options.filename file: options.cssOutputFilename || options.filename
}) }),
hasGlobal: analysis.css.has_global
}; };
merge_with_preprocessor_map(css, options, css.map.sources[0]); merge_with_preprocessor_map(css, options, css.map.sources[0]);
@ -169,7 +170,11 @@ const visitors = {
if (node.metadata.is_global_block) { if (node.metadata.is_global_block) {
const selector = node.prelude.children[0]; 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 {...}` // `:global {...}`
if (state.minify) { if (state.minify) {
state.code.remove(node.start, node.block.start + 1); state.code.remove(node.start, node.block.start + 1);
@ -191,9 +196,12 @@ const visitors = {
next(); next();
}, },
SelectorList(node, { state, next, path }) { 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 // Only add comments if we're not inside a complex selector that itself is unused or a global block
if ( 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) !path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) { ) {
const children = node.children; const children = node.children;
@ -255,7 +263,6 @@ const visitors = {
// if this selector list belongs to a rule, require a specificity bump for the // 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 // first scoped selector but only if we're at the top level
let parent = path.at(-1);
if (parent?.type === 'Rule') { if (parent?.type === 'Rule') {
specificity = { bumped: false }; specificity = { bumped: false };
@ -281,14 +288,25 @@ const visitors = {
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]); const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state); remove_global_pseudo_class(global, relative_selector.combinator, context.state);
if ( const parent_rule = node.metadata.rule?.metadata.parent_rule;
node.metadata.rule?.metadata.parent_rule && if (parent_rule && global.args === null) {
global.args === null && if (relative_selector.combinator === null) {
relative_selector.combinator === null
) {
// div { :global.x { ... } } becomes div { &.x { ... } } // div { :global.x { ... } } becomes div { &.x { ... } }
context.state.code.prependRight(global.start, '&'); 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; continue;
} else { } else {
// for any :global() or :global at the middle of compound selector // for any :global() or :global at the middle of compound selector
@ -360,7 +378,6 @@ const visitors = {
}; };
/** /**
*
* @param {Array<AST.CSS.Node>} path * @param {Array<AST.CSS.Node>} path
*/ */
function is_in_global_block(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 // div :global.x becomes div.x
while (/\s/.test(state.code.original[start - 1])) start--; 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 { } else {
state.code state.code
.remove(selector.start, selector.start + ':global('.length) .remove(selector.start, selector.start + ':global('.length)

@ -5,7 +5,7 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js'; import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.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 { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js'; import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js';
@ -186,12 +186,10 @@ export function server_component(analysis, options) {
...snippets, ...snippets,
b.let('$$settled', b.true), b.let('$$settled', b.true),
b.let('$$inner_payload'), b.let('$$inner_payload'),
b.stmt( b.function_declaration(
b.function(
b.id('$$render_inner'), b.id('$$render_inner'),
[b.id('$$payload')], [b.id('$$payload')],
b.block(/** @type {Statement[]} */ (rest)) b.block(/** @type {Statement[]} */ (rest))
)
), ),
b.do_while( b.do_while(
b.unary('!', b.id('$$settled')), b.unary('!', b.id('$$settled')),

@ -1,7 +1,7 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */ /** @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 { build_assignment_value } from '../../../../utils/ast.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';

@ -1,8 +1,8 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */ /** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js'; import { block_close } from './shared/utils.js';
/** /**
* @param {AST.AwaitBlock} node * @param {AST.AwaitBlock} node
@ -10,10 +10,10 @@ import { empty_comment } from './shared/utils.js';
*/ */
export function AwaitBlock(node, context) { export function AwaitBlock(node, context) {
context.state.template.push( context.state.template.push(
empty_comment,
b.stmt( b.stmt(
b.call( b.call(
'$.await', '$.await',
b.id('$$payload'),
/** @type {Expression} */ (context.visit(node.expression)), /** @type {Expression} */ (context.visit(node.expression)),
b.thunk( b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
@ -21,13 +21,9 @@ export function AwaitBlock(node, context) {
b.arrow( b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [], node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([]) 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
); );
} }

@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, CallExpression, Expression } from 'estree' */ /** @import { ArrowFunctionExpression, CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.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 { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js'; import { transform_inspect_rune } from '../../utils.js';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save