Merge remote-tracking branch 'upstream/main' into $state-invalidate

pull/15673/head
ComputerGuy 1 year ago
commit 814e089c81

@ -8,10 +8,8 @@ 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
- run: corepack enable
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 22.x node-version: 22.x

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

@ -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();
@ -219,4 +219,4 @@ This is useful for linking elements via attributes like `for` and `aria-labelled
<label for="{uid}-lastname">Last Name: </label> <label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" /> <input id="{uid}-lastname" type="text" />
</form> </form>
``` ```

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

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

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

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

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

@ -1,5 +1,63 @@
# svelte # svelte
## 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 ## 5.25.10
### Patch Changes ### Patch Changes

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

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

@ -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.10", "version": "5.27.0",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -573,12 +573,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`);
} }
/** /**

@ -944,54 +944,53 @@ 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 bindings = ids.map((id) => state.scope.get(id.name));
const reassigned_bindings = bindings.filter((b) => b?.reassigned);
if ( const ids = extract_identifiers(left);
node.body.expression.right.type !== 'Literal' && const [, expression_ids] = extract_all_identifiers_from_expression(right);
!bindings.some((b) => b?.kind === 'store_sub') && const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name)));
node.body.expression.left.type !== 'MemberExpression'
) {
let { start, end } = /** @type {{ start: number, end: number }} */ (
node.body.expression.right
);
check_rune_binding('derived'); if (bindings.every((b) => b.kind === 'legacy_reactive')) {
if (
right.type !== 'Literal' &&
bindings.every((b) => b.kind !== 'store_sub') &&
left.type !== 'MemberExpression'
) {
let { start, end } = /** @type {{ start: number, end: number }} */ (right);
// $derived check_rune_binding('derived');
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.body.expression.start),
'let '
);
if (node.body.expression.right.type === 'SequenceExpression') { // $derived
while (state.str.original[start] !== '(') start -= 1; state.str.update(
while (state.str.original[end - 1] !== ')') end += 1; /** @type {number} */ (node.start),
} /** @type {number} */ (node.body.expression.start),
'let '
);
if (right.type === 'SequenceExpression') {
while (state.str.original[start] !== '(') start -= 1;
while (state.str.original[end - 1] !== ')') end += 1;
}
state.str.prependRight(start, `$derived(`);
state.str.prependRight(start, `$derived(`); // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
// otherwise, we need to add one
if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
state.str.appendLeft(end, `)`);
}
// in a case like `$: ({ a } = b())`, there's already a trailing parenthesis. return;
// otherwise, we need to add one
if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
state.str.appendLeft(end, `)`);
} }
return; for (const binding of bindings) {
} else { if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
for (const binding of reassigned_bindings) {
if (binding && (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
} }
}; };

@ -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[];
* { * rule: AST.CSS.Rule | null;
* keyframes: string[]; * analysis: ComponentAnalysis;
* rule: AST.CSS.Rule | null; * }} CssState
* } */
* >} CssVisitors
/**
* @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);
} }
} }
@ -99,10 +113,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 +193,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 +206,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 &&
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { selector_idx === 0 &&
ComplexSelector(node) { node.metadata.parent_rule === null
node.metadata.used = true; ) {
} e.css_global_block_invalid_modifier_start(child.selectors[1]);
}); } else {
} // `child` starts with `:global`
} node.metadata.is_global_block = is_global_block = true;
}
for (let i = 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
return is_global_block; if (child.combinator && child.combinator.name !== ' ') {
}); e.css_global_block_invalid_combinator(child, child.combinator.name);
}
if (node.metadata.is_global_block) { const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (node.prelude.children.length > 1) { const is_lone_global =
e.css_global_block_invalid_list(node.prelude); complex_selector.children.length === 1 &&
} complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
const complex_selector = node.prelude.children[0]; if (is_lone_global && node.prelude.children.length > 1) {
const global_selector = complex_selector.children.find((r, selector_idx) => { // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
const idx = r.selectors.findIndex(is_global_block_selector); // constrained by `x`, which is not what the user intended
if (idx === 0) { e.css_global_block_invalid_list(node.prelude);
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) { }
e.css_global_block_invalid_modifier_start(r.selectors[1]);
if (
declaration &&
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 &&
is_lone_global
) {
e.css_global_block_invalid_declaration(declaration);
}
} }
return true;
} else if (idx !== -1) { } 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 +316,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);
} }

@ -456,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(),

@ -7,6 +7,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 +112,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 (

@ -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);
} }

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

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.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);

@ -89,21 +89,21 @@ export function build_template_chunk(
} }
} }
const is_defined = const evaluated = state.scope.evaluate(value);
value.type === 'BinaryExpression' ||
(value.type === 'UnaryExpression' && value.operator !== 'void') ||
(value.type === 'LogicalExpression' && value.right.type === 'Literal') ||
(value.type === 'Identifier' && value.name === state.analysis.props_id?.name);
if (!is_defined) {
// add `?? ''` where necessary (TODO optimise more cases)
value = b.logical('??', value, b.literal(''));
}
expressions.push(value); if (evaluated.is_known) {
quasi.value.cooked += evaluated.value + '';
} else {
if (!evaluated.is_defined) {
// add `?? ''` where necessary
value = b.logical('??', value, b.literal(''));
}
quasi = b.quasi('', i + 1 === values.length); expressions.push(value);
quasis.push(quasi);
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
}
} }
} }

@ -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);
@ -193,7 +198,7 @@ const visitors = {
SelectorList(node, { state, next, path }) { SelectorList(node, { state, next, path }) {
// 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) &&
!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;
@ -281,13 +286,24 @@ 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 { ... } }
) { context.state.code.prependRight(global.start, '&');
// div { :global.x { ... } } becomes div { &.x { ... } } }
context.state.code.prependRight(global.start, '&');
// In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if
// the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector
if (
parent_rule.prelude.children.length > 1 &&
node.children.length === node.children.findIndex((s) => s === relative_selector) - 1
) {
const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end);
if (next_selector && next_selector.metadata.used) {
context.state.code.update(global.end, next_selector.start, '');
}
}
} }
continue; continue;
} else { } else {
@ -379,7 +395,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)

@ -2,7 +2,7 @@
/** @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 '../../../../utils/builders.js';
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,6 +1,7 @@
/** @import { BlockStatement } from 'estree' */ /** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
/** /**
@ -13,7 +14,9 @@ export function SnippetBlock(node, context) {
[b.id('$$payload'), ...node.parameters], [b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body)) /** @type {BlockStatement} */ (context.visit(node.body))
); );
if (dev) {
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true; fn.___snippet = true;

@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) {
if (node.type === 'Text' || node.type === 'Comment') { if (node.type === 'Text' || node.type === 'Comment') {
quasi.value.cooked += quasi.value.cooked +=
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data); node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data);
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += escape_html(node.expression.value + '');
}
} else { } else {
expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); const evaluated = state.scope.evaluate(node.expression);
if (evaluated.is_known) {
quasi.value.cooked += escape_html((evaluated.value ?? '') + '');
} else {
expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
quasi = b.quasi('', i + 1 === sequence.length); quasi = b.quasi('', i + 1 === sequence.length);
quasis.push(quasi); quasis.push(quasi);
}
} }
} }

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */ /** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
@ -16,6 +16,11 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js'; import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
export const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
export class Binding { export class Binding {
/** @type {Scope} */ /** @type {Scope} */
scope; scope;
@ -34,7 +39,7 @@ export class Binding {
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
* @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
*/ */
initial; initial = null;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = []; references = [];
@ -100,6 +105,264 @@ export class Binding {
} }
} }
class Evaluation {
/** @type {Set<any>} */
values = new Set();
/**
* True if there is exactly one possible value
* @readonly
* @type {boolean}
*/
is_known = true;
/**
* True if the value is known to not be null/undefined
* @readonly
* @type {boolean}
*/
is_defined = true;
/**
* True if the value is known to be a string
* @readonly
* @type {boolean}
*/
is_string = true;
/**
* True if the value is known to be a number
* @readonly
* @type {boolean}
*/
is_number = true;
/**
* @readonly
* @type {any}
*/
value = undefined;
/**
*
* @param {Scope} scope
* @param {Expression} expression
*/
constructor(scope, expression) {
switch (expression.type) {
case 'Literal': {
this.values.add(expression.value);
break;
}
case 'Identifier': {
const binding = scope.get(expression.name);
if (binding) {
if (
binding.initial?.type === 'CallExpression' &&
get_rune(binding.initial, scope) === '$props.id'
) {
this.values.add(STRING);
break;
}
const is_prop =
binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'bindable_prop';
if (!binding.updated && binding.initial !== null && !is_prop) {
const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial));
for (const value of evaluation.values) {
this.values.add(value);
}
break;
}
// TODO each index is always defined
}
// TODO glean what we can from reassignments
// TODO one day, expose props and imports somehow
this.values.add(UNKNOWN);
break;
}
case 'BinaryExpression': {
const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in`
const b = scope.evaluate(expression.right);
if (a.is_known && b.is_known) {
this.values.add(binary[expression.operator](a.value, b.value));
break;
}
switch (expression.operator) {
case '!=':
case '!==':
case '<':
case '<=':
case '>':
case '>=':
case '==':
case '===':
case 'in':
case 'instanceof':
this.values.add(true);
this.values.add(false);
break;
case '%':
case '&':
case '*':
case '**':
case '-':
case '/':
case '<<':
case '>>':
case '>>>':
case '^':
case '|':
this.values.add(NUMBER);
break;
case '+':
if (a.is_string || b.is_string) {
this.values.add(STRING);
} else if (a.is_number && b.is_number) {
this.values.add(NUMBER);
} else {
this.values.add(STRING);
this.values.add(NUMBER);
}
break;
default:
this.values.add(UNKNOWN);
}
break;
}
case 'ConditionalExpression': {
const test = scope.evaluate(expression.test);
const consequent = scope.evaluate(expression.consequent);
const alternate = scope.evaluate(expression.alternate);
if (test.is_known) {
for (const value of (test.value ? consequent : alternate).values) {
this.values.add(value);
}
} else {
for (const value of consequent.values) {
this.values.add(value);
}
for (const value of alternate.values) {
this.values.add(value);
}
}
break;
}
case 'LogicalExpression': {
const a = scope.evaluate(expression.left);
const b = scope.evaluate(expression.right);
if (a.is_known) {
if (b.is_known) {
this.values.add(logical[expression.operator](a.value, b.value));
break;
}
if (
(expression.operator === '&&' && !a.value) ||
(expression.operator === '||' && a.value) ||
(expression.operator === '??' && a.value != null)
) {
this.values.add(a.value);
} else {
for (const value of b.values) {
this.values.add(value);
}
}
break;
}
for (const value of a.values) {
this.values.add(value);
}
for (const value of b.values) {
this.values.add(value);
}
break;
}
case 'UnaryExpression': {
const argument = scope.evaluate(expression.argument);
if (argument.is_known) {
this.values.add(unary[expression.operator](argument.value));
break;
}
switch (expression.operator) {
case '!':
case 'delete':
this.values.add(false);
this.values.add(true);
break;
case '+':
case '-':
case '~':
this.values.add(NUMBER);
break;
case 'typeof':
this.values.add(STRING);
break;
case 'void':
this.values.add(undefined);
break;
default:
this.values.add(UNKNOWN);
}
break;
}
default: {
this.values.add(UNKNOWN);
}
}
for (const value of this.values) {
this.value = value; // saves having special logic for `size === 1`
if (value !== STRING && typeof value !== 'string') {
this.is_string = false;
}
if (value !== NUMBER && typeof value !== 'number') {
this.is_number = false;
}
if (value == null || value === UNKNOWN) {
this.is_defined = false;
}
}
if (this.values.size > 1 || typeof this.value === 'symbol') {
this.is_known = false;
}
}
}
export class Scope { export class Scope {
/** @type {ScopeRoot} */ /** @type {ScopeRoot} */
root; root;
@ -279,8 +542,63 @@ export class Scope {
this.root.conflicts.add(node.name); this.root.conflicts.add(node.name);
} }
} }
/**
* Does partial evaluation to find an exact value or at least the rough type of the expression.
* Only call this once scope has been fully generated in a first pass,
* else this evaluates on incomplete data and may yield wrong results.
* @param {Expression} expression
* @param {Set<any>} values
*/
evaluate(expression, values = new Set()) {
return new Evaluation(this, expression);
}
} }
/** @type {Record<BinaryOperator, (left: any, right: any) => any>} */
const binary = {
'!=': (left, right) => left != right,
'!==': (left, right) => left !== right,
'<': (left, right) => left < right,
'<=': (left, right) => left <= right,
'>': (left, right) => left > right,
'>=': (left, right) => left >= right,
'==': (left, right) => left == right,
'===': (left, right) => left === right,
in: (left, right) => left in right,
instanceof: (left, right) => left instanceof right,
'%': (left, right) => left % right,
'&': (left, right) => left & right,
'*': (left, right) => left * right,
'**': (left, right) => left ** right,
'+': (left, right) => left + right,
'-': (left, right) => left - right,
'/': (left, right) => left / right,
'<<': (left, right) => left << right,
'>>': (left, right) => left >> right,
'>>>': (left, right) => left >>> right,
'^': (left, right) => left ^ right,
'|': (left, right) => left | right
};
/** @type {Record<UnaryOperator, (argument: any) => any>} */
const unary = {
'-': (argument) => -argument,
'+': (argument) => +argument,
'!': (argument) => !argument,
'~': (argument) => ~argument,
typeof: (argument) => typeof argument,
void: () => undefined,
delete: () => true
};
/** @type {Record<LogicalOperator, (left: any, right: any) => any>} */
const logical = {
'||': (left, right) => left || right,
'&&': (left, right) => left && right,
'??': (left, right) => left ?? right
};
export class ScopeRoot { export class ScopeRoot {
/** @type {Set<string>} */ /** @type {Set<string>} */
conflicts = new Set(); conflicts = new Set();

@ -74,6 +74,7 @@ export interface ComponentAnalysis extends Analysis {
ast: AST.CSS.StyleSheet | null; ast: AST.CSS.StyleSheet | null;
hash: string; hash: string;
keyframes: string[]; keyframes: string[];
has_global: boolean;
}; };
source: string; source: string;
undefined_exports: Map<string, Node>; undefined_exports: Map<string, Node>;

@ -34,6 +34,10 @@ export namespace _CSS {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
/**
* `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like
*/
has_global_selectors: boolean;
/** /**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/ */
@ -64,6 +68,7 @@ export namespace _CSS {
/** @internal */ /** @internal */
metadata: { metadata: {
rule: null | Rule; rule: null | Rule;
is_global: boolean;
/** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */
used: boolean; used: boolean;
}; };

@ -18,6 +18,8 @@ export interface CompileResult {
code: string; code: string;
/** A source map */ /** A source map */
map: SourceMap; map: SourceMap;
/** Whether or not the CSS includes global rules */
hasGlobal: boolean;
}; };
/** /**
* An array of warning objects that were generated during compilation. Each warning has several properties: * An array of warning objects that were generated during compilation. Each warning has several properties:

@ -641,11 +641,13 @@ export function reactive_declaration_module_script_dependency(node) {
} }
/** /**
* State referenced in its own scope will never update. Did you mean to reference it inside a closure? * This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
* @param {null | NodeLike} node * @param {null | NodeLike} node
* @param {string} name
* @param {string} type
*/ */
export function state_referenced_locally(node) { export function state_referenced_locally(node, name, type) {
w(node, 'state_referenced_locally', `State referenced in its own scope will never update. Did you mean to reference it inside a closure?\nhttps://svelte.dev/e/state_referenced_locally`); w(node, 'state_referenced_locally', `This reference only captures the initial value of \`${name}\`. Did you mean to reference it inside a ${type} instead?\nhttps://svelte.dev/e/state_referenced_locally`);
} }
/** /**

@ -22,6 +22,7 @@ export const HYDRATION_START = '[';
/** used to indicate that an `{:else}...` block was rendered */ /** used to indicate that an `{:else}...` block was rendered */
export const HYDRATION_START_ELSE = '[!'; export const HYDRATION_START_ELSE = '[!';
export const HYDRATION_END = ']'; export const HYDRATION_END = ']';
export const HYDRATION_AWAIT_THEN = '!';
export const HYDRATION_ERROR = {}; export const HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_IS_NAMESPACED = 1;

@ -27,17 +27,18 @@ export function create_ownership_validator(props) {
*/ */
mutation: (prop, path, result, line, column) => { mutation: (prop, path, result, line, column) => {
const name = path[0]; const name = path[0];
if (is_bound(props, name) || !parent) { if (is_bound_or_unset(props, name) || !parent) {
return result; return result;
} }
let value = props[name]; /** @type {any} */
let value = props;
for (let i = 1; i < path.length - 1; i++) { for (let i = 0; i < path.length - 1; i++) {
value = value[path[i]];
if (!value?.[STATE_SYMBOL]) { if (!value?.[STATE_SYMBOL]) {
return result; return result;
} }
value = value[path[i]];
} }
const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`); const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`);
@ -52,7 +53,7 @@ export function create_ownership_validator(props) {
* @param {() => any} value * @param {() => any} value
*/ */
binding: (key, child_component, value) => { binding: (key, child_component, value) => {
if (!is_bound(props, key) && parent && value()?.[STATE_SYMBOL]) { if (!is_bound_or_unset(props, key) && parent && value()?.[STATE_SYMBOL]) {
w.ownership_invalid_binding( w.ownership_invalid_binding(
component[FILENAME], component[FILENAME],
key, key,
@ -68,9 +69,13 @@ export function create_ownership_validator(props) {
* @param {Record<string, any>} props * @param {Record<string, any>} props
* @param {string} prop_name * @param {string} prop_name
*/ */
function is_bound(props, prop_name) { function is_bound_or_unset(props, prop_name) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)` // or `createClassComponent(Component, props)`
const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
return !!get_descriptor(props, prop_name)?.set || (is_entry_props && prop_name in props); return (
!!get_descriptor(props, prop_name)?.set ||
(is_entry_props && prop_name in props) ||
!(prop_name in props)
);
} }

@ -0,0 +1,15 @@
import { invalid_snippet_arguments } from '../../shared/errors.js';
/**
* @param {Node} anchor
* @param {...(()=>any)[]} args
*/
export function validate_snippet_args(anchor, ...args) {
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
invalid_snippet_arguments();
}
for (let arg of args) {
if (typeof arg !== 'function') {
invalid_snippet_arguments();
}
}
}

@ -4,9 +4,16 @@ import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import {
hydrate_next,
hydrate_node,
hydrating,
remove_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { import {
component_context, component_context,
is_runes, is_runes,
@ -113,6 +120,19 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var effect = block(() => { var effect = block(() => {
if (input === (input = get_input())) return; if (input === (input = get_input())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = remove_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
if (is_promise(input)) { if (is_promise(input)) {
var promise = input; var promise = input;
@ -155,6 +175,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
update(THEN, false); update(THEN, false);
} }
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}
// Set the input to something else, in order to disable the promise callbacks // Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED); return () => (input = UNINITIALIZED);
}); });

@ -8,6 +8,7 @@ export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js'; export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js'; export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js'; export { inspect } from './dev/inspect.js';
export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js'; export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js'; export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js'; export { key_block as key } from './dom/blocks/key.js';

@ -7,7 +7,7 @@ import {
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED PROPS_IS_UPDATED
} from '../../../constants.js'; } from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js'; import { define_property, get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js'; import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js'; import { derived, derived_safe_equal } from './deriveds.js';
import { get, captured_signals, untrack } from '../runtime.js'; import { get, captured_signals, untrack } from '../runtime.js';

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ /** @import { Derived, Effect, Source, Value } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
active_reaction, active_reaction,
@ -12,7 +12,6 @@ import {
increment_write_version, increment_write_version,
update_effect, update_effect,
reaction_sources, reaction_sources,
set_reaction_sources,
check_dirtiness, check_dirtiness,
untracking, untracking,
is_destroying_effect, is_destroying_effect,
@ -162,7 +161,6 @@ export function internal_set(source, value) {
} }
source.v = value; source.v = value;
source.wv = increment_write_version();
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt'); source.updated = get_stack('UpdatedAt');
@ -180,6 +178,8 @@ export function internal_set(source, value) {
set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
} }
source.wv = increment_write_version();
mark_reactions(source, DIRTY); mark_reactions(source, DIRTY);
// It's possible that the current reaction might not have up-to-date dependencies // It's possible that the current reaction might not have up-to-date dependencies

@ -94,18 +94,11 @@ export function set_active_effect(effect) {
*/ */
export let reaction_sources = null; export let reaction_sources = null;
/**
* @param {Source[] | null} sources
*/
export function set_reaction_sources(sources) {
reaction_sources = sources;
}
/** @param {Value} value */ /** @param {Value} value */
export function push_reaction_value(value) { export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) { if (reaction_sources === null) {
set_reaction_sources([value]); reaction_sources = [value];
} else { } else {
reaction_sources.push(value); reaction_sources.push(value);
} }

@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */ /** @import { Snippet } from 'svelte' */
/** @import { Payload } from '#server' */ /** @import { Payload } from '../payload' */
/** @import { Getters } from '#shared' */ /** @import { Getters } from '#shared' */
/** /**

@ -1,10 +1,12 @@
/** @import { Component, Payload } from '#server' */ /** @import { Component } from '#server' */
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { import {
is_tag_valid_with_ancestor, is_tag_valid_with_ancestor,
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../html-tree-validation.js'; } from '../../html-tree-validation.js';
import { current_component } from './context.js'; import { current_component } from './context.js';
import { invalid_snippet_arguments } from '../shared/errors.js';
import { HeadPayload, Payload } from './payload.js';
/** /**
* @typedef {{ * @typedef {{
@ -98,3 +100,16 @@ export function push_element(payload, tag, line, column) {
export function pop_element() { export function pop_element() {
parent = /** @type {Element} */ (parent).parent; parent = /** @type {Element} */ (parent).parent;
} }
/**
* @param {Payload} payload
*/
export function validate_snippet_args(payload) {
if (
typeof payload !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
) {
invalid_snippet_arguments();
}
}

@ -1,5 +1,5 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */ /** @import { ComponentType, SvelteComponent } from 'svelte' */
/** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Component, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */ /** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js'; export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
@ -13,47 +13,17 @@ import {
import { escape_html } from '../../escaping.js'; import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js'; import { current_component, pop, push } from './context.js';
import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js'; import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js'; import { reset_elements } from './dev.js';
import { Payload } from './payload.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter // https://infra.spec.whatwg.org/#noncharacter
const INVALID_ATTR_NAME_CHAR_REGEX = const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
/**
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head, uid }) {
return {
out,
css: new Set(css),
head: {
title: head.title,
out: head.out,
css: new Set(head.css),
uid: head.uid
},
uid
};
}
/**
* Assigns second payload to first
* @param {Payload} p1
* @param {Payload} p2
* @returns {void}
*/
export function assign_payload(p1, p2) {
p1.out = p2.out;
p1.css = p2.css;
p1.head = p2.head;
p1.uid = p2.uid;
}
/** /**
* @param {Payload} payload * @param {Payload} payload
* @param {string} tag * @param {string} tag
@ -87,16 +57,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/ */
export let on_destroy = []; export let on_destroy = [];
/**
* Creates an ID generator
* @param {string} prefix
* @returns {() => string}
*/
function props_id_generator(prefix) {
let uid = 1;
return () => `${prefix}s${uid++}`;
}
/** /**
* Only available on the server and when compiling with the `server` option. * Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
@ -106,14 +66,7 @@ function props_id_generator(prefix) {
* @returns {RenderOutput} * @returns {RenderOutput}
*/ */
export function render(component, options = {}) { export function render(component, options = {}) {
const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : ''); const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
/** @type {Payload} */
const payload = {
out: '',
css: new Set(),
head: { title: '', out: '', css: new Set(), uid },
uid
};
const prev_on_destroy = on_destroy; const prev_on_destroy = on_destroy;
on_destroy = []; on_destroy = [];
@ -474,18 +427,21 @@ export function bind_props(props_parent, props_now) {
/** /**
* @template V * @template V
* @param {Payload} payload
* @param {Promise<V>} promise * @param {Promise<V>} promise
* @param {null | (() => void)} pending_fn * @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn * @param {(value: V) => void} then_fn
* @returns {void} * @returns {void}
*/ */
function await_block(promise, pending_fn, then_fn) { function await_block(payload, promise, pending_fn, then_fn) {
if (is_promise(promise)) { if (is_promise(promise)) {
payload.out += BLOCK_OPEN;
promise.then(null, noop); promise.then(null, noop);
if (pending_fn !== null) { if (pending_fn !== null) {
pending_fn(); pending_fn();
} }
} else if (then_fn !== null) { } else if (then_fn !== null) {
payload.out += BLOCK_OPEN_ELSE;
then_fn(promise); then_fn(promise);
} }
} }
@ -542,7 +498,9 @@ export { html } from './blocks/html.js';
export { push, pop } from './context.js'; export { push, pop } from './context.js';
export { push_element, pop_element } from './dev.js'; export { push_element, pop_element, validate_snippet_args } from './dev.js';
export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';

@ -0,0 +1,72 @@
export class HeadPayload {
/** @type {Set<{ hash: string; code: string }>} */
css = new Set();
out = '';
uid = () => '';
title = '';
constructor(css = new Set(), out = '', title = '', uid = () => '') {
this.css = css;
this.out = out;
this.title = title;
this.uid = uid;
}
}
export class Payload {
/** @type {Set<{ hash: string; code: string }>} */
css = new Set();
out = '';
uid = () => '';
head = new HeadPayload();
constructor(id_prefix = '') {
this.uid = props_id_generator(id_prefix);
this.head.uid = this.uid;
}
}
/**
* Used in legacy mode to handle bindings
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head, uid }) {
const payload = new Payload();
payload.out = out;
payload.css = new Set(css);
payload.uid = uid;
payload.head = new HeadPayload();
payload.head.out = head.out;
payload.head.css = new Set(head.css);
payload.head.title = head.title;
payload.head.uid = head.uid;
return payload;
}
/**
* Assigns second payload to first
* @param {Payload} p1
* @param {Payload} p2
* @returns {void}
*/
export function assign_payload(p1, p2) {
p1.out = p2.out;
p1.css = p2.css;
p1.head = p2.head;
p1.uid = p2.uid;
}
/**
* Creates an ID generator
* @param {string} prefix
* @returns {() => string}
*/
function props_id_generator(prefix) {
let uid = 1;
return () => `${prefix}s${uid++}`;
}

@ -11,19 +11,6 @@ export interface Component {
function?: any; function?: any;
} }
export interface Payload {
out: string;
css: Set<{ hash: string; code: string }>;
head: {
title: string;
out: string;
uid: () => string;
css: Set<{ hash: string; code: string }>;
};
/** Function that generates a unique ID */
uid: () => string;
}
export interface RenderOutput { export interface RenderOutput {
/** HTML that goes into the `<head>` */ /** HTML that goes into the `<head>` */
head: string; head: string;

@ -17,6 +17,21 @@ export function invalid_default_snippet() {
} }
} }
/**
* A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
* @returns {never}
*/
export function invalid_snippet_arguments() {
if (DEV) {
const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
}
}
/** /**
* `%name%(...)` can only be used during component initialisation * `%name%(...)` can only be used during component initialisation
* @param {string} name * @param {string} name

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.25.10'; export const VERSION = '5.27.0';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message:
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
position: [232, 246]
}
});

@ -0,0 +1,9 @@
<style>
/* valid */
/* We gotta allow `:global x, :global y` and the likes because CSS preprocessors might generate that from e.g. `:global { x, y {...} }` */
:global .x, :global .y {}
.x :global, .y :global {}
/* invalid */
.x :global, .y {}
</style>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message:
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
position: [24, 43]
}
});

@ -0,0 +1,6 @@
<style>
/* invalid */
:global, :global .y {
z { color: red }
}
</style>

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message: 'A `:global` selector cannot be part of a selector list with more than one item',
position: [9, 31]
}
});

@ -16,6 +16,20 @@ export default test({
column: 16, column: 16,
character: 932 character: 932
} }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "unused :global"',
start: {
line: 100,
column: 29,
character: 1223
},
end: {
line: 100,
column: 43,
character: 1237
}
} }
] ]
}); });

@ -90,3 +90,13 @@
opacity: 1; opacity: 1;
} }
} }
x, y {
color: green;
}
div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ {
z {
color: green;
}
}

@ -92,4 +92,14 @@
opacity: 1; opacity: 1;
} }
} }
:global x, :global y {
color: green;
}
div :global, div :global y, unused :global {
z {
color: green;
}
}
</style> </style>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: true
});

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: false
});

@ -0,0 +1,12 @@
div.svelte-xyz {
.whatever {
color: green;
}
}
.whatever {
div.svelte-xyz {
color: green;
}
}

@ -0,0 +1,15 @@
<div>{@html whatever}</div>
<style>
div {
:global(.whatever) {
color: green;
}
}
:global(.whatever) {
div {
color: green;
}
}
</style>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: false
});

@ -0,0 +1,8 @@
div.svelte-xyz .whatever {
color: green;
}
.whatever div.svelte-xyz {
color: green;
}

@ -0,0 +1,11 @@
<div>{@html whatever}</div>
<style>
div :global(.whatever) {
color: green;
}
:global(.whatever) div {
color: green;
}
</style>

@ -1,5 +1,7 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
warnings: [] warnings: [],
hasGlobal: false
}); });

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: true
});

@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial<CompileOptions>; compileOptions?: Partial<CompileOptions>;
warnings?: Warning[]; warnings?: Warning[];
props?: Record<string, any>; props?: Record<string, any>;
hasGlobal?: boolean;
} }
/** /**
@ -78,6 +79,14 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
// assert_html_equal(actual_ssr, expected.html); // assert_html_equal(actual_ssr, expected.html);
} }
if (config.hasGlobal !== undefined) {
const metadata = JSON.parse(
fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
);
assert.equal(metadata.hasGlobal, config.hasGlobal);
}
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim(); const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim(); const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();

@ -146,6 +146,10 @@ export async function compile_directory(
if (compiled.css) { if (compiled.css) {
write(`${output_dir}/${file}.css`, compiled.css.code); write(`${output_dir}/${file}.css`, compiled.css.code);
write(
`${output_dir}/${file}.css.json`,
JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
);
if (output_map) { if (output_map) {
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
} }

@ -0,0 +1,6 @@
<script>
let something = '123';
let foo = false;
$: foo = !!something;
</script>

@ -0,0 +1,10 @@
<script>
import { run } from 'svelte/legacy';
let something = '123';
let foo = $state(false);
run(() => {
foo = !!something;
});
</script>

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
ssrHtml: '<button>fulfil</button><p>42</p><hr><p>loading...</p>',
html: '<button>fulfil</button><p>loading...</p><hr><p>42</p>',
props: {
browser: true
},
server_props: {
browser: false
},
async test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<button>fulfil</button><p>42</p><hr><p>42</p>');
}
});

@ -0,0 +1,25 @@
<script>
let { browser } = $props();
let fulfil;
let promise = new Promise((f) => (fulfil = f));
let a = browser ? promise : 42;
let b = browser ? 42 : promise;
</script>
<button onclick={() => fulfil(42)}>fulfil</button>
{#await a}
{#if true}<p>loading...</p>{/if}
{:then a}
<p>{a}</p>
{/await}
<hr>
{#await b}
{#if true}<p>loading...</p>{/if}
{:then b}
<p>{b}</p>
{/await}

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
mode: ['server'],
async test({ errors, assert }) {
assert.equal(errors, []);
}
});

@ -0,0 +1,7 @@
{#snippet head()}
<title>Cool</title>
{/snippet}
<svelte:head>
{@render head()}
</svelte:head>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
test({ assert, target, warnings }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.deepEqual(warnings, []);
},
warnings: []
});

@ -0,0 +1,8 @@
<script>
let { klass, getter_setter } = $props();
</script>
<button onclick={() => {
klass.y = 2;
getter_setter.y = 2;
}}>mutate</button>

@ -0,0 +1,21 @@
<script>
import Child from './child.svelte';
class X {
y = $state(1);
}
const klass = new X();
let y = $state(1);
const getter_setter = {
get y() {
return y;
},
set y(value) {
y = value;
}
}
</script>
<Child {klass} {getter_setter} />

@ -0,0 +1,5 @@
<script>
const { test = $bindable() } = $props();
</script>
{test}

@ -0,0 +1,7 @@
<script>
import Child from './Child.svelte';
let { test = $bindable({}) } = $props();
</script>
<Child bind:test />

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,5 @@
<script>
import Parent from './Parent.svelte';
</script>
<Parent />

@ -0,0 +1,8 @@
<script>
let { test = $bindable({}) } = $props();
</script>
<button onclick={()=>test = {}}></button>
<button onclick={()=>test.test = {}}></button>
{test}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert, target }) {
const [btn, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn2.click();
});
assert.deepEqual(warnings, []);
flushSync(() => {
btn.click();
});
flushSync(() => {
btn2.click();
});
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,5 @@
<script>
import Parent from './Parent.svelte';
</script>
<Parent />

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>default</p>`
});

@ -0,0 +1,5 @@
{#snippet test(param = "default")}
<p>{param}</p>
{/snippet}
{@render test()}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
html: `true true`,
test({ assert, target, window }) {}
});

@ -0,0 +1,9 @@
<script>
import { expect2, createAppState } from "./util.svelte.js"
const result = createAppState({ source: () => "wrong" });
result.onChange("right");
const expect1 = result.value === "right";
</script>
{expect1} {expect2}

@ -0,0 +1,17 @@
export const createAppState = (options) => {
const source = $derived(options.source());
let value = $derived(source);
return {
get value() {
return value;
},
onChange(nextValue) {
value = nextValue;
}
};
};
const result = createAppState({ source: () => 'wrong' });
result.onChange('right');
export const expect2 = result.value === 'right';

@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) {
counter.count += 1; counter.count += 1;
} }
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> <!---->`; $$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> `;
$.await(promise, () => {}, (counter) => {}, () => {}); $.await($$payload, promise, () => {}, (counter) => {});
$$payload.out += `<!----> ${$.escape(counter.count)}`; $$payload.out += `<!--]--> ${$.escape(counter.count)}`;
} }

@ -10,11 +10,11 @@ export default function Nullish_coallescence_omittance($$anchor) {
var fragment = root(); var fragment = root();
var h1 = $.first_child(fragment); var h1 = $.first_child(fragment);
h1.textContent = `Hello, ${name ?? ''}!`; h1.textContent = 'Hello, world!';
var b = $.sibling(h1, 2); var b = $.sibling(h1, 2);
b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`; b.textContent = '123';
var button = $.sibling(b, 2); var button = $.sibling(b, 2);
@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) {
var h1_1 = $.sibling(button, 2); var h1_1 = $.sibling(button, 2);
h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`; h1_1.textContent = 'Hello, world';
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
$.append($$anchor, fragment); $.append($$anchor, fragment);
} }

@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) {
let name = 'world'; let name = 'world';
let count = 0; let count = 0;
$$payload.out += `<h1>Hello, ${$.escape(name)}!</h1> <b>${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')}</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, ${$.escape(name ?? 'earth' ?? null)}</h1>`; $$payload.out += `<h1>Hello, world!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, world</h1>`;
} }

@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var select = $.sibling(div_1, 2); var select = $.sibling(div_1, 2);
var option = $.child(select); var option = $.child(select);
option.value = null == (option.__value = 'a') ? '' : 'a'; option.value = option.__value = 'a';
$.reset(select); $.reset(select);
var img = $.sibling(select, 2); var img = $.sibling(select, 2);

@ -8,4 +8,4 @@
replace_me_script = 'hello' replace_me_script = 'hello'
; ;
</script> </script>
<h1 class="done_replace_style_2">{done_replace_script_2}</h1> <h1 class="done_replace_style_2">{Math.random() < 1 && done_replace_script_2}</h1>

@ -1,6 +1,7 @@
<!-- valid --> <!-- valid -->
<link /> <link />
<svg><g /></svg> <svg><g /></svg>
<math><mspace /></math>
<enhanced:img /> <enhanced:img />

@ -3,11 +3,11 @@
"code": "element_invalid_self_closing_tag", "code": "element_invalid_self_closing_tag",
"message": "Self-closing HTML tags for non-void elements are ambiguous — use `<div ...></div>` rather than `<div ... />`", "message": "Self-closing HTML tags for non-void elements are ambiguous — use `<div ...></div>` rather than `<div ... />`",
"start": { "start": {
"line": 8, "line": 9,
"column": 0 "column": 0
}, },
"end": { "end": {
"line": 8, "line": 9,
"column": 7 "column": 7
} }
}, },
@ -15,11 +15,11 @@
"code": "element_invalid_self_closing_tag", "code": "element_invalid_self_closing_tag",
"message": "Self-closing HTML tags for non-void elements are ambiguous — use `<my-thing ...></my-thing>` rather than `<my-thing ... />`", "message": "Self-closing HTML tags for non-void elements are ambiguous — use `<my-thing ...></my-thing>` rather than `<my-thing ... />`",
"start": { "start": {
"line": 9, "line": 10,
"column": 0 "column": 0
}, },
"end": { "end": {
"line": 9, "line": 10,
"column": 12 "column": 12
} }
} }

@ -2,6 +2,7 @@
let obj = $state({ a: 0 }); let obj = $state({ a: 0 });
let count = $state(0); let count = $state(0);
let doubled = $derived(count * 2); let doubled = $derived(count * 2);
let tripled = $state(count * 3);
console.log(obj); console.log(obj);
console.log(count); console.log(count);

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

Loading…
Cancel
Save