Merge remote-tracking branch 'origin/main' into state-onchange

pull/15579/head
paoloricciuti 6 months ago
commit df62dd67f3

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: value/checked not correctly set using spread

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: SSR-safe ID generation with `$props.id()`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: use `importNode` to clone templates for Firefox

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: recurse into `$derived` for ownership validation

@ -12,6 +12,7 @@ env:
jobs:
Tests:
permissions: {}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
@ -41,6 +42,7 @@ jobs:
env:
CI: true
Lint:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@ -61,6 +63,7 @@ jobs:
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); }
Benchmarks:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:

@ -9,6 +9,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/github-script@v6
with:
script: |

@ -6,11 +6,15 @@ on:
types:
- completed
permissions:
pull-requests: write
jobs:
build:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
with:

@ -3,6 +3,8 @@ on: [push, pull_request]
jobs:
build:
permissions: {}
runs-on: ubuntu-latest
steps:

@ -17,6 +17,7 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@v4
with:

@ -1,4 +1,4 @@
Copyright (c) 2016-2025 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -5,7 +5,7 @@
</picture>
</a>
[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
[![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
## What is Svelte?

@ -20,12 +20,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === len + i);

@ -28,13 +28,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);

@ -22,13 +22,13 @@ function setup() {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === i * size);

@ -38,13 +38,13 @@ function setup() {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
}

@ -51,11 +51,11 @@ function setup() {
*/
run(i) {
res.length = 0;
$.flush_sync(() => {
$.flush(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
$.flush_sync(() => {
$.flush(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});

@ -2,7 +2,7 @@
title: What are runes?
---
> [!NOTE] **rune** /ro͞on/ _noun_
> [!NOTE] **rune** /ruːn/ _noun_
>
> A letter or mark used as a mystical or magic symbol.

@ -2,15 +2,11 @@
title: $effect
---
Effects are what make your application _do things_. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `<canvas>` elements, or making network requests. They only run in the browser, not during server-side rendering.
Most of the effects in a Svelte app are created by Svelte itself — they're the bits that update the text in `<h1>hello {name}!</h1>` when `name` changes, for example.
Generally speaking, you should _not_ update state inside effects, as it will make code more convoluted and will often lead to never-ending update cycles. If you find yourself doing so, see [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
But you can also create your own effects with the `$effect` rune, which is useful when you need to synchronize an external system (whether that's a library, or a `<canvas>` element, or something across a network) with state inside your Svelte app.
> [!NOTE] Avoid overusing `$effect`! When you do too much work in effects, code often becomes difficult to understand and maintain. See [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
```svelte
<script>
@ -32,11 +28,19 @@ Your effects run after the component has been mounted to the DOM, and in a [micr
<canvas bind:this={canvas} width="100" height="100" />
```
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.
When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
> [!NOTE] If you're having difficulty understanding why your `$effect` is rerunning or is not running see [understanding dependencies](#Understanding-dependencies). Effects are triggered differently than the `$:` blocks you may be used to if coming from Svelte 4.
### Understanding lifecycle
You can place `$effect` anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect is destroyed).
Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes. 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.
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/playground/untitled#H4sIAAAAAAAAE42RQY-bMBCF_8rI2kPopiXpMQtIPfbeW6m0xjyKtWaM7CFphPjvFVB2k2oPe7LmzXzyezOjaqxDVKefo5JrD3VaBLVXrLu5-tb3X-IZTmat0hHv6cazgCWqk8qiCbaXouRSHISMH1gop4coWrA7JE9bp7PO2QjjuY5vA8fDYZ3hUh7QNDCy2yWUFzTOUilpSj9aG-linaMKFGACtKCmSwvGGYGeLQvCWbtnMq3m34grajxHoa1JOUXI93_V_Sfz7Oz7Mafj0ypN-zvHm8dSAmQITP_xaUq2IU1GO1dp80I2Uh_82dao92Rl9R8GvgF0QrbrUFstcFeq0PgAkha0LoICPoeB4w1SJUvsZcj4rvcMlvmvGlGCv6J-DeSgw2vabQnJlm55p7nM0rcTctYei3HZxZSl7XHVqkHEM3k2zpqXfFyj393zU05fpyI6f0HI0hUoPoamC9roKDeo2ivBH1EnCQOmX9NfYw2GHrgCAAA=)).
You can use `$effect` anywhere, not just at the top level of a component, as long as it is called while a parent effect is running.
> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.
An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)).
```svelte
<script>
@ -50,7 +54,7 @@ You can return a function from `$effect`, which will run immediately before the
}, milliseconds);
return () => {
// if a callback is provided, it will run
// if a teardown function is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
clearInterval(interval);
@ -64,9 +68,11 @@ You can return a function from `$effect`, which will run immediately before the
<button onclick={() => (milliseconds /= 2)}>faster</button>
```
Teardown functions also run when the effect is destroyed, which happens when its parent is destroyed (for example, a component is unmounted) or the parent effect re-runs.
### Understanding dependencies
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run.
Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):

@ -33,7 +33,7 @@ Now, a component that uses `<FancyInput>` can add the [`bind:`](bind) directive
<!-- prettier-ignore -->
```svelte
/// App.svelte
/// file: App.svelte
<script>
import FancyInput from './FancyInput.svelte';

@ -185,7 +185,7 @@ You can use HTML comments inside components.
Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually, these are accessibility warnings; make sure that you're disabling them for a good reason.
```svelte
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input bind:value={name} autofocus />
```

@ -267,7 +267,7 @@ Elements with the `contenteditable` attribute support the following bindings:
<!-- for some reason puts the comment and html on same line -->
<!-- prettier-ignore -->
```svelte
<div contenteditable="true" bind:innerHTML={html} />
<div contenteditable="true" bind:innerHTML={html}></div>
```
## Dimensions
@ -307,7 +307,7 @@ To get a reference to a DOM node, use `bind:this`. The value will be `undefined`
});
</script>
<canvas bind:this={canvas} />
<canvas bind:this={canvas}></canvas>
```
Components also support `bind:this`, allowing you to interact with component instances programmatically.

@ -147,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru
}
function toggle() {
toggleValue = !toggleValue;
theme = theme === 'dark' ? 'light' : 'dark';
}
</script>

@ -10,13 +10,13 @@ You don't have to migrate to the new syntax right away - Svelte 5 still supports
At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign.
### let -> $state
### let $state
In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`:
```svelte
<script>
let count = +++$state(+++0+++)+++;
let count = +++$state(0)+++;
</script>
```
@ -25,14 +25,14 @@ Nothing else changes. `count` is still the number itself, and you read and write
> [!DETAILS] Why we did this
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
### $: -> $derived/$effect
### $: $derived/$effect
In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune:
```svelte
<script>
let count = +++$state(+++0+++)+++;
---$:--- +++const+++ double = +++$derived(+++count * 2+++)+++;
let count = $state(0);
---$:--- +++const+++ double = +++$derived(count * 2)+++;
</script>
```
@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
```svelte
<script>
let count = +++$state(+++0+++)+++;
let count = $state(0);
---$:---+++$effect(() =>+++ {
if (count > 5) {
alert('Count is too high!');
@ -51,6 +52,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
</script>
```
Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs.
> [!DETAILS] Why we did this
> `$:` was a great shorthand and easy to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect? With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time.
>
@ -71,14 +74,14 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
> - executing dependencies as needed and therefore being immune to ordering problems
> - being TypeScript-friendly
### export let -> $props
### export let $props
In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring:
```svelte
<script>
---export let optional = 'unset';
export let required;---
---export let optional = 'unset';---
---export let required;---
+++let { optional = 'unset', required } = $props();+++
</script>
```
@ -103,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
```svelte
<script>
---let klass = '';
export { klass as class};---
---let klass = '';---
---export { klass as class};---
+++let { class: klass, ...rest } = $props();+++
</script>
<button class={klass} {...---$$restProps---+++rest+++}>click me</button>
@ -190,9 +193,9 @@ This function is deprecated in Svelte 5. Instead, components should accept _call
```svelte
<!--- file: Pump.svelte --->
<script>
---import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
---
---import { createEventDispatcher } from 'svelte';---
---const dispatch = createEventDispatcher();---
+++let { inflate, deflate } = $props();+++
let power = $state(5);
</script>
@ -464,11 +467,11 @@ By now you should have a pretty good understanding of the before/after and how t
We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things:
- bump core dependencies in your `package.json`
- migrate to runes (`let` -> `$state` etc)
- migrate to event attributes for DOM elements (`on:click` -> `onclick`)
- migrate slot creations to render tags (`<slot />` -> `{@render children()}`)
- migrate slot usages to snippets (`<div slot="x">...</div>` -> `{#snippet x()}<div>...</div>{/snippet}`)
- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`)
- migrate to runes (`let` `$state` etc)
- migrate to event attributes for DOM elements (`on:click` `onclick`)
- migrate slot creations to render tags (`<slot />` `{@render children()}`)
- migrate slot usages to snippets (`<div slot="x">...</div>` `{#snippet x()}<div>...</div>{/snippet}`)
- migrate obvious component creations (`new Component(...)` `mount(Component, ...)`)
You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button.

@ -46,7 +46,7 @@ It will show up on hover.
- You can use markdown here.
- You can also use code blocks here.
- Usage:
```tsx
```svelte
<main name="Arethra">
```
-->

@ -133,3 +133,27 @@ Reading state that was created inside the same derived is forbidden. Consider us
```
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
```
This error is thrown in a situation like this:
```svelte
<script>
let count = $state(0);
let multiple = $derived.by(() => {
const result = count * 2;
if (result > 10) {
count = 0;
}
return result;
});
</script>
<button onclick={() => count++}>{count} / {multiple}</button>
```
Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable.
To fix this:
- See if it's possible to refactor your `$derived` such that the update becomes unnecessary
- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update
- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates

@ -84,6 +84,12 @@ Attribute values containing `{...}` must be enclosed in quote marks, unless the
`bind:group` can only bind to an Identifier or MemberExpression
```
### bind_group_invalid_snippet_parameter
```
Cannot `bind:group` to a snippet parameter
```
### bind_invalid_expression
```

@ -42,7 +42,7 @@
"prettier-plugin-svelte": "^3.1.2",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"typescript-eslint": "^8.24.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
}

@ -1,5 +1,170 @@
# svelte
## 5.23.1
### Patch Changes
- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506))
- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277))
- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511))
- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313))
- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473))
- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492))
- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516))
## 5.23.0
### Minor Changes
- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469))
## 5.22.6
### Patch Changes
- fix: skip `log_if_contains_state` if only logging literals ([#15468](https://github.com/sveltejs/svelte/pull/15468))
- fix: Add `closedby` property to HTMLDialogAttributes type ([#15458](https://github.com/sveltejs/svelte/pull/15458))
- fix: null and warnings for local handlers ([#15460](https://github.com/sveltejs/svelte/pull/15460))
## 5.22.5
### Patch Changes
- fix: memoize `clsx` calls ([#15456](https://github.com/sveltejs/svelte/pull/15456))
- fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes ([#15443](https://github.com/sveltejs/svelte/pull/15443))
- fix: always use `setAttribute` when setting `style` ([#15323](https://github.com/sveltejs/svelte/pull/15323))
- fix: make `style:` directive and CSS handling more robust ([#15418](https://github.com/sveltejs/svelte/pull/15418))
## 5.22.4
### Patch Changes
- fix: never deduplicate expressions in templates ([#15451](https://github.com/sveltejs/svelte/pull/15451))
## 5.22.3
### Patch Changes
- fix: run effect roots in tree order ([#15446](https://github.com/sveltejs/svelte/pull/15446))
## 5.22.2
### Patch Changes
- fix: correctly set `is_updating` before flushing root effects ([#15442](https://github.com/sveltejs/svelte/pull/15442))
## 5.22.1
### Patch Changes
- chore: switch acorn-typescript plugin ([#15393](https://github.com/sveltejs/svelte/pull/15393))
## 5.22.0
### Minor Changes
- feat: Add `idPrefix` option to `render` ([#15428](https://github.com/sveltejs/svelte/pull/15428))
### Patch Changes
- fix: make dialog element and role interactive ([#15429](https://github.com/sveltejs/svelte/pull/15429))
## 5.21.0
### Minor Changes
- chore: Reduce hydration comment for {:else if} ([#15250](https://github.com/sveltejs/svelte/pull/15250))
### Patch Changes
- fix: disallow `bind:group` to snippet parameters ([#15401](https://github.com/sveltejs/svelte/pull/15401))
## 5.20.5
### Patch Changes
- fix: allow double hyphen css selector names ([#15384](https://github.com/sveltejs/svelte/pull/15384))
- fix: class:directive not working with $restProps #15386 ([#15389](https://github.com/sveltejs/svelte/pull/15389))
fix: spread add an useless cssHash on non-scoped element
- fix: catch error on @const tag in svelte:boundary in DEV mode ([#15369](https://github.com/sveltejs/svelte/pull/15369))
- fix: allow for duplicate `var` declarations ([#15382](https://github.com/sveltejs/svelte/pull/15382))
- fix : bug "$0 is not defined" on svelte:element with a function call on class ([#15396](https://github.com/sveltejs/svelte/pull/15396))
## 5.20.4
### Patch Changes
- fix: update types and inline docs for flushSync ([#15348](https://github.com/sveltejs/svelte/pull/15348))
## 5.20.3
### Patch Changes
- fix: allow `@const` inside `#key` ([#15377](https://github.com/sveltejs/svelte/pull/15377))
- fix: remove unnecessary `?? ''` on some expressions ([#15287](https://github.com/sveltejs/svelte/pull/15287))
- fix: correctly override class attributes with class directives ([#15352](https://github.com/sveltejs/svelte/pull/15352))
## 5.20.2
### Patch Changes
- chore: remove unused `options.uid` in `render` ([#15302](https://github.com/sveltejs/svelte/pull/15302))
- fix: do not warn for `binding_property_non_reactive` if binding is a store in an each ([#15318](https://github.com/sveltejs/svelte/pull/15318))
- fix: prevent writable store value from becoming a proxy when reassigning using $-prefix ([#15283](https://github.com/sveltejs/svelte/pull/15283))
- fix: `muted` reactive without `bind` and select/autofocus attributes working with function calls ([#15326](https://github.com/sveltejs/svelte/pull/15326))
- fix: ensure input elements and elements with `dir` attribute are marked as non-static ([#15259](https://github.com/sveltejs/svelte/pull/15259))
- fix: fire delegated events on target even it was disabled in the meantime ([#15319](https://github.com/sveltejs/svelte/pull/15319))
## 5.20.1
### Patch Changes
- fix: ensure AST analysis on `svelte.js` modules succeeds ([#15297](https://github.com/sveltejs/svelte/pull/15297))
- fix: ignore typescript abstract methods ([#15267](https://github.com/sveltejs/svelte/pull/15267))
- fix: correctly ssr component in `svelte:head` with `$props.id()` or `css='injected'` ([#15291](https://github.com/sveltejs/svelte/pull/15291))
## 5.20.0
### Minor Changes
- feat: SSR-safe ID generation with `$props.id()` ([#15185](https://github.com/sveltejs/svelte/pull/15185))
### Patch Changes
- fix: take private and public into account for `constant_assignment` of derived state ([#15276](https://github.com/sveltejs/svelte/pull/15276))
- fix: value/checked not correctly set using spread ([#15239](https://github.com/sveltejs/svelte/pull/15239))
- chore: tweak effect self invalidation logic, run transition dispatches without reactive context ([#15275](https://github.com/sveltejs/svelte/pull/15275))
- fix: use `importNode` to clone templates for Firefox ([#15272](https://github.com/sveltejs/svelte/pull/15272))
- fix: recurse into `$derived` for ownership validation ([#15166](https://github.com/sveltejs/svelte/pull/15166))
## 5.19.10
### Patch Changes

@ -957,6 +957,7 @@ export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {
export interface HTMLDialogAttributes extends HTMLAttributes<HTMLDialogElement> {
open?: boolean | undefined | null;
closedby?: 'any' | 'closerequest' | 'none' | undefined | null;
}
export interface HTMLEmbedAttributes extends HTMLAttributes<HTMLEmbedElement> {
@ -1075,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
checked?: boolean | undefined | null;
dirname?: string | undefined | null;
disabled?: boolean | undefined | null;
files?: FileList | undefined | null;
form?: string | undefined | null;
formaction?: string | undefined | null;
formenctype?:
@ -1086,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null;
formnovalidate?: boolean | undefined | null;
formtarget?: string | undefined | null;
group?: any | undefined | null;
height?: number | string | undefined | null;
indeterminate?: boolean | undefined | null;
list?: string | undefined | null;

@ -87,3 +87,27 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
## state_unsafe_mutation
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
This error is thrown in a situation like this:
```svelte
<script>
let count = $state(0);
let multiple = $derived.by(() => {
const result = count * 2;
if (result > 10) {
count = 0;
}
return result;
});
</script>
<button onclick={() => count++}>{count} / {multiple}</button>
```
Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable.
To fix this:
- See if it's possible to refactor your `$derived` such that the update becomes unnecessary
- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update
- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates

@ -54,6 +54,10 @@
> `bind:group` can only bind to an Identifier or MemberExpression
## bind_group_invalid_snippet_parameter
> Cannot `bind:group` to a snippet parameter
## bind_invalid_expression
> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair

@ -2,18 +2,19 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.19.10",
"version": "5.23.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
"node": ">=18"
},
"files": [
"*.d.ts",
"src",
"!src/**/*.test.*",
"!src/**/*.d.ts",
"types",
"compiler",
"*.d.ts",
"README.md"
],
"module": "src/index-client.js",
@ -137,11 +138,11 @@
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.3",
"dts-buddy": "^0.5.5",
"esbuild": "^0.21.5",
"rollup": "^4.22.4",
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9"
},
@ -150,7 +151,7 @@
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",

@ -762,6 +762,15 @@ export function bind_group_invalid_expression(node) {
e(node, 'bind_group_invalid_expression', `\`bind:group\` can only bind to an Identifier or MemberExpression\nhttps://svelte.dev/e/bind_group_invalid_expression`);
}
/**
* Cannot `bind:group` to a snippet parameter
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function bind_group_invalid_snippet_parameter(node) {
e(node, 'bind_group_invalid_snippet_parameter', `Cannot \`bind:group\` to a snippet parameter\nhttps://svelte.dev/e/bind_group_invalid_snippet_parameter`);
}
/**
* Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
* @param {null | number | NodeLike} node

@ -1,11 +1,9 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Node } from 'acorn' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript';
import { locator } from '../../state.js';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @param {string} source
@ -48,7 +46,6 @@ export function parse(source, typescript, is_script) {
}
}
if (typescript) amend(source, ast);
add_comments(ast);
return /** @type {Program} */ (ast);
@ -71,7 +68,6 @@ export function parse_expression_at(source, typescript, index) {
locations: true
});
if (typescript) amend(source, ast);
add_comments(ast);
return ast;
@ -173,42 +169,3 @@ function get_comment_handlers(source) {
}
};
}
/**
* Tidy up some stuff left behind by acorn-typescript
* @param {string} source
* @param {Node} node
*/
function amend(source, node) {
return walk(node, null, {
_(node, context) {
// @ts-expect-error
delete node.loc.start.index;
// @ts-expect-error
delete node.loc.end.index;
if (typeof node.loc?.end === 'number') {
const loc = locator(node.loc.end);
if (loc) {
node.loc.end = {
line: loc.line,
column: loc.column
};
}
}
if (
/** @type {any} */ (node).typeAnnotation &&
(node.end === undefined || node.end < node.start)
) {
// i think there might be a bug in acorn-typescript that prevents
// `end` from being assigned when there's a type annotation
let end = /** @type {any} */ (node).typeAnnotation.start;
while (/\s/.test(source[end - 1])) end -= 1;
node.end = end;
}
context.next();
}
});
}

@ -1,3 +0,0 @@
// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias
// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files
declare module 'acorn-typescript';

@ -566,7 +566,7 @@ function read_attribute_value(parser) {
}
/**
* https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
* https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
* @param {Parser} parser
*/
function read_identifier(parser) {
@ -574,7 +574,7 @@ function read_identifier(parser) {
let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
if (parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
e.css_expected_identifier(start);
}

@ -118,6 +118,12 @@ const visitors = {
delete node.implements;
return context.next();
},
MethodDefinition(node, context) {
if (node.abstract) {
return b.empty;
}
return context.next();
},
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;

@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) {
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
}
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
// BSD Licensed
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3;

@ -133,7 +133,13 @@ const css_visitors = {
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
if (node.selectors.length === 1) {
if (
node.selectors.length >= 1 &&
node.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
) {
const first = node.selectors[0];
node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') ||

@ -5,9 +5,12 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
const FORWARD = 0;
const BACKWARD = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
@ -43,6 +46,27 @@ const nesting_selector = {
}
};
/** @type {Compiler.AST.CSS.RelativeSelector} */
const any_selector = {
type: 'RelativeSelector',
start: -1,
end: -1,
combinator: null,
selectors: [
{
type: 'TypeSelector',
name: '*',
start: -1,
end: -1
}
],
metadata: {
is_global: false,
is_global_like: false,
scoped: false
}
};
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set<Compiler.AST.SnippetBlock>}
@ -72,7 +96,8 @@ export function prune(stylesheet, element) {
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element
element,
BACKWARD
)
) {
node.metadata.used = true;
@ -159,16 +184,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element) {
const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop();
function apply_selector(relative_selectors, rule, element, direction) {
const rest_selectors = relative_selectors.slice();
const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched =
!!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
apply_combinator(relative_selector, parent_selectors, rule, element);
relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) {
if (!is_outer_global(relative_selector)) {
@ -183,76 +209,63 @@ function apply_selector(relative_selectors, rule, element) {
/**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
* @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Direction} direction
* @returns {boolean}
*/
function apply_combinator(relative_selector, parent_selectors, rule, node) {
if (!relative_selector.combinator) return true;
const name = relative_selector.combinator.name;
function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
const combinator =
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
if (!combinator) return true;
switch (name) {
switch (combinator.name) {
case ' ':
case '>': {
const is_adjacent = combinator.name === '>';
const parents =
direction === FORWARD
? get_descendant_elements(node, is_adjacent)
: get_ancestor_elements(node, is_adjacent);
let parent_matched = false;
const path = node.metadata.path;
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'SnippetBlock') {
if (seen.has(parent)) {
for (const parent of parents) {
if (apply_selector(rest_selectors, rule, parent, direction)) {
parent_matched = true;
} else {
seen.add(parent);
for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
parent_matched = true;
}
}
}
break;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent)) {
parent_matched = true;
}
if (name === '>') return parent_matched;
}
}
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
return (
parent_matched ||
(direction === BACKWARD &&
(!is_adjacent || parents.length === 0) &&
rest_selectors.every((selector) => is_global(selector, rule)))
);
}
case '+':
case '~': {
const siblings = get_possible_element_siblings(node, name === '+');
const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true;
}
} else if (apply_selector(parent_selectors, rule, possible_sibling)) {
} else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
(get_element_parent(node) === null &&
parent_selectors.every((selector) => is_global(selector, rule)))
(direction === BACKWARD &&
get_element_parent(node) === null &&
rest_selectors.every((selector) => is_global(selector, rule)))
);
}
@ -313,9 +326,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, rule, element) {
function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
@ -331,13 +345,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const child_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const descendant_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
@ -353,46 +360,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
)
)
);
if (include_self) {
child_elements.push(element);
descendant_elements.push(element);
}
const seen = new Set();
/**
* @param {Compiler.AST.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
walk(node, state, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendant_elements.push(node);
if (context.state.is_child) {
child_elements.push(node);
context.state.is_child = false;
context.next();
context.state.is_child = true;
} else {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body, context.state);
}
} else {
context.next();
}
}
});
}
walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -403,36 +370,33 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let matched = false;
for (const complex_selector of complex_selectors) {
const selectors = truncate(complex_selector);
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
// In .x:has(> y), we want to search for y, ignoring the left-most combinator
// (else it would try to walk further up and fail because there are no selectors left)
if (selectors.length > 0) {
selectors[0] = {
...selectors[0],
combinator: null
};
const [first, ...rest] = truncate(complex_selector);
// if it was just a :global(...)
if (!first) {
complex_selector.metadata.used = true;
matched = true;
continue;
}
const descendants =
left_most_combinator.name === '+' || left_most_combinator.name === '~'
? (sibling_elements ??= get_following_sibling_elements(element, include_self))
: left_most_combinator.name === '>'
? child_elements
: descendant_elements;
let selector_matched = false;
// Iterate over all descendant elements and check if the selector inside :has matches
for (const element of descendants) {
if (
selectors.length === 0 /* is :global(...) */ ||
(element.metadata.scoped && selector_matched) ||
apply_selector(selectors, rule, element)
) {
if (include_self) {
const selector_including_self = [
first.combinator ? { ...first, combinator: null } : first,
...rest
];
if (apply_selector(selector_including_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
selector_matched = matched = true;
matched = true;
}
}
const selector_excluding_self = [
any_selector,
first.combinator ? first : { ...first, combinator: descendant_combinator },
...rest
];
if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
matched = true;
}
}
@ -458,7 +422,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) {
const args = selector.args;
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element);
return apply_selector(complex_selector.children, rule, element, BACKWARD);
}
// We came across a :global, everything beyond it is global and therefore a potential match
@ -507,7 +471,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
} else if (apply_selector(relative, rule, element)) {
} else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true;
matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@ -591,7 +555,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
for (const complex_selector of parent.prelude.children) {
if (
apply_selector(get_relative_selectors(complex_selector), parent, element) ||
apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true;
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {boolean} include_self
*/
function get_following_sibling_elements(element, include_self) {
const path = element.metadata.path;
let i = path.length;
/** @type {Compiler.AST.SvelteNode} */
let start = element;
let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
/** @type {Compiler.AST.Fragment} */ (path[0]).nodes
);
// find the set of nodes to walk...
while (i--) {
const node = path[i];
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
nodes = node.fragment.nodes;
break;
}
if (node.type !== 'Fragment') {
start = node;
}
}
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const siblings = [];
// ...then walk them, starting from the node containing the element in question
// skipping nodes that appears before the element
const seen = new Set();
let skip = true;
/** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
SvelteElement(node) {
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
get_siblings(snippet.body);
}
}
});
}
for (const node of nodes.slice(nodes.indexOf(start))) {
get_siblings(node);
}
return siblings;
}
/**
* @param {any} operator
* @param {any} expected_value
@ -822,6 +712,84 @@ function unquote(str) {
return str;
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/
function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const ancestors = [];
const path = node.metadata.path;
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'SnippetBlock') {
if (!seen.has(parent)) {
seen.add(parent);
for (const site of parent.metadata.sites) {
ancestors.push(...get_ancestor_elements(site, adjacent_only, seen));
}
}
break;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
ancestors.push(parent);
if (adjacent_only) {
break;
}
}
}
return ancestors;
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/
function get_descendant_elements(node, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const descendants = [];
/**
* @param {Compiler.AST.SvelteNode} node
*/
function walk_children(node) {
walk(node, null, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendants.push(node);
if (!adjacent_only) {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body);
}
} else {
context.next();
}
}
});
}
walk_children(node.type === 'RenderTag' ? node : node.fragment);
return descendants;
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
@ -843,11 +811,12 @@ function get_element_parent(node) {
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map();
const path = node.metadata.path;
@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
let j = fragment.nodes.indexOf(current);
let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1);
while (j--) {
while (j >= 0 && j < fragment.nodes.length) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
@ -876,23 +845,30 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result;
}
}
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
} else if (is_block(node)) {
if (node.type === 'SlotElement') {
result.set(node, NODE_PROBABLY_EXISTS);
}
const possible_last_child = get_possible_last_child(node, adjacent_only);
const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
} else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
} else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
} else if (node.type === 'RenderTag') {
result.set(node, NODE_PROBABLY_EXISTS);
for (const snippet of node.metadata.snippets) {
add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
}
}
j = direction === FORWARD ? j + 1 : j - 1;
}
current = path[i];
if (!current) break;
@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current);
for (const site of current.metadata.sites) {
const siblings = get_possible_element_siblings(site, adjacent_only, seen);
const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
@ -923,7 +899,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(current, adjacent_only), result);
add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result);
}
}
@ -931,11 +907,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
}
/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock} node
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>}
*/
function get_possible_last_child(node, adjacent_only) {
function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.Fragment | undefined | null>} */
let fragments = [];
@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement':
fragments.push(node.fragment);
break;
case 'SnippetBlock':
if (seen.has(node)) {
return new Map();
}
seen.add(node);
fragments.push(node.body);
break;
}
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */
const result = new Map();
let exhaustive = node.type !== 'SlotElement';
let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) {
if (fragment == null) {
@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) {
continue;
}
const map = loop_child(fragment.nodes, adjacent_only);
const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map);
add_to_map(map, result);
@ -1012,27 +998,28 @@ function add_to_map(from, to) {
}
/**
* @param {NodeExistsValue | undefined} exist1
* @param {NodeExistsValue} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
// @ts-expect-error TODO figure out if this is a bug
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
if (exist2 === undefined) return exist1;
return exist1 > exist2 ? exist1 : exist2;
}
/**
* @param {Compiler.AST.SvelteNode[]} children
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/
function loop_child(children, adjacent_only) {
function loop_child(children, direction, adjacent_only, seen) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */
const result = new Map();
let i = children.length;
let i = direction === FORWARD ? 0 : children.length - 1;
while (i--) {
while (i >= 0 && i < children.length) {
const child = children[i];
if (child.type === 'RegularElement') {
@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) {
}
} else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS);
} else if (child.type === 'RenderTag') {
for (const snippet of child.metadata.snippets) {
add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result);
}
} else if (is_block(child)) {
const child_result = get_possible_last_child(child, adjacent_only);
const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
i = direction === FORWARD ? i + 1 : i - 1;
}
return result;

@ -258,8 +258,18 @@ export function analyze_module(ast, options) {
{
scope,
scopes,
// @ts-expect-error TODO
analysis
analysis: /** @type {ComponentAnalysis} */ (analysis),
derived_state: [],
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
component_slots: new Set(),
expression: null,
function_depth: 0,
has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options),
parent_element: null,
reactive_statement: null
},
visitors
);
@ -554,7 +564,7 @@ export function analyze_component(root, source, options) {
binding.declaration_kind !== 'import'
) {
binding.kind = 'state';
binding.mutated = binding.updated = true;
binding.mutated = true;
}
}
}
@ -608,9 +618,7 @@ export function analyze_component(root, source, options) {
expression: null,
derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: new Map()
reactive_statement: null
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -672,9 +680,7 @@ export function analyze_component(root, source, options) {
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
derived_state: [],
@ -755,66 +761,62 @@ export function analyze_component(root, source, options) {
if (!should_ignore_unused) {
warn_unused(analysis.css.ast);
}
}
outer: for (const node of analysis.elements) {
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
for (const node of analysis.elements) {
if (node.metadata.scoped && is_custom_element_node(node)) {
mark_subtree_dynamic(node.metadata.path);
}
/** @type {AST.Attribute | undefined} */
let class_attribute = undefined;
let has_class = false;
let has_style = false;
let has_spread = false;
let has_class_directive = false;
let has_style_directive = false;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
if (attribute.type === 'SpreadAttribute') {
has_spread = true;
break;
} else if (attribute.type === 'Attribute') {
has_class ||= attribute.name.toLowerCase() === 'class';
has_style ||= attribute.name.toLowerCase() === 'style';
} else if (attribute.type === 'ClassDirective') {
has_class_directive = true;
} else if (attribute.type === 'StyleDirective') {
has_style_directive = true;
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
if (is_text_attribute(class_attribute)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`;
} else {
/** @type {AST.Text} */
const css_text = {
// We need an empty class to generate the set_class() or class="" correctly
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
data: '',
raw: '',
start: -1,
end: -1
};
if (Array.isArray(class_attribute.value)) {
class_attribute.value.push(css_text);
} else {
class_attribute.value = [class_attribute.value, css_text];
}
])
);
}
} else {
// We need an empty style to generate the set_style() correctly
if (!has_spread && !has_style && has_style_directive) {
node.attributes.push(
create_attribute('class', -1, -1, [
create_attribute('style', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
data: '',
raw: '',
start: -1,
end: -1
}
])
);
if (is_custom_element_node(node) && node.attributes.length === 1) {
mark_subtree_dynamic(node.metadata.path);
}
}
}
}
}

@ -1,7 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
@ -19,13 +18,11 @@ export interface AnalysisState {
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
derived_state: string[];
derived_state: { name: string; private: boolean }[];
function_depth: number;
// legacy stuff
instance_scope: Scope;
reactive_statement: null | ReactiveStatement;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -162,18 +162,10 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
const binding_type = binding.initial.type;
if (
binding_type === 'ArrowFunctionExpression' ||
binding_type === 'FunctionDeclaration' ||
binding_type === 'FunctionExpression'
) {
if (binding?.is_function()) {
target_function = binding.initial;
}
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {

@ -5,7 +5,7 @@ import {
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_no_const_assignment } from './shared/utils.js';
import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
validate_no_const_assignment(node, node.expression, context.state.scope, true);
validate_assignment(node, node.expression, context.state);
const assignee = node.expression;
const left = object(assignee);
@ -184,14 +184,6 @@ export function BindDirective(node, context) {
) {
e.bind_invalid_value(node.expression);
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
@ -199,6 +191,10 @@ export function BindDirective(node, context) {
throw new Error('Cannot find declaration for bind:group');
}
if (binding.kind === 'snippet') {
e.bind_group_invalid_snippet_parameter(node);
}
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block

@ -42,6 +42,9 @@ export function CallExpression(node, context) {
e.bindable_invalid_location(node);
}
// We need context in case the bound prop is stale
context.state.analysis.needs_context = true;
break;
case '$host':
@ -218,14 +221,6 @@ export function CallExpression(node, context) {
break;
}
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);
if (binding !== null) {
binding.is_called = true;
}
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });

@ -7,7 +7,7 @@ import { get_rune } from '../../scope.js';
* @param {Context} context
*/
export function ClassBody(node, context) {
/** @type {string[]} */
/** @type {{name: string, private: boolean}[]} */
const derived_state = [];
for (const definition of node.body) {
@ -18,7 +18,10 @@ export function ClassBody(node, context) {
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
derived_state.push(definition.key.name);
derived_state.push({
name: definition.key.name,
private: definition.key.type === 'PrivateIdentifier'
});
}
}
}

@ -25,6 +25,7 @@ export function ConstTag(node, context) {
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
grand_parent?.type !== 'SvelteBoundary' &&
grand_parent?.type !== 'KeyBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {

@ -22,7 +22,7 @@ export function ExportSpecifier(node, context) {
});
const binding = context.state.scope.get(local_name);
if (binding) binding.reassigned = binding.updated = true;
if (binding) binding.reassigned = true;
}
} else {
validate_export(node, context.state.scope, local_name);

@ -64,7 +64,7 @@ export function LabeledStatement(node, context) {
}
}
context.state.reactive_statements.set(node, reactive_statement);
context.state.analysis.reactive_statements.set(node, reactive_statement);
if (
node.body.type === 'ExpressionStatement' &&

@ -1,4 +1,4 @@
/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */
/** @import { TaggedTemplateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_pure } from './shared/utils.js';
@ -12,12 +12,5 @@ export function TaggedTemplateExpression(node, context) {
context.state.expression.has_state = true;
}
if (node.tag.type === 'Identifier') {
const binding = context.state.scope.get(node.tag.name);
if (binding !== null) {
binding.is_called = true;
}
}
context.next();
}

@ -30,7 +30,7 @@ const non_interactive_roles = non_abstract_roles
// 'generic' is meant to have no semantic meaning.
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
!role?.superClass.some((classes) => classes.includes('widget'))
!role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
);
})
.concat(

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AssignmentExpression, Expression, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
@ -10,12 +10,12 @@ import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
@ -38,16 +38,22 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
state.derived_state.includes(argument.property.name)) ||
state.derived_state.some(
(derived) =>
derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name &&
derived.private === (argument.property.type === 'PrivateIdentifier')
)) ||
(argument.property.type === 'Literal' &&
argument.property.value &&
typeof argument.property.value === 'string' &&
state.derived_state.includes(argument.property.value)))
state.derived_state.some(
(derived) =>
derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
)))
) {
e.constant_assignment(node, 'derived state');
}

@ -315,7 +315,7 @@ export function client_component(analysis, options) {
const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flush_sync'))
b.stmt(b.call('$.flush'))
]);
if (analysis.runes && binding.initial) {
@ -599,7 +599,7 @@ export function client_component(analysis, options) {
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
prop_def.reflect ? b.init('reflect', b.true) : undefined,
prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
].filter(Boolean)
)

@ -45,6 +45,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
readonly store_to_invalidate?: string;
/** Stuff that happens before the render effect(s) */
readonly init: Statement[];

@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js';
export function AnimateDirective(node, context) {
const expression =
node.expression === null
? b.literal(null)
? b.null
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this

@ -119,6 +119,7 @@ function build_assignment(operator, left, right, context) {
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'store_sub' &&
context.state.analysis.runes &&
should_proxy(right, context.state.scope) &&
is_non_coercive_operator(operator)

@ -60,7 +60,7 @@ export function AwaitBlock(node, context) {
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.literal(null),
: b.null,
then_block,
catch_block
)

@ -16,7 +16,7 @@ export function BinaryExpression(node, context) {
'$.strict_equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
operator === '!==' && b.literal(false)
operator === '!==' && b.false
);
}
@ -25,7 +25,7 @@ export function BinaryExpression(node, context) {
'$.equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
operator === '!=' && b.literal(false)
operator === '!=' && b.false
);
}
}

@ -44,7 +44,8 @@ export function CallExpression(node, context) {
node.callee.property.type === 'Identifier' &&
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
)
) &&
node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
) {
return b.call(
node.callee,

@ -143,7 +143,8 @@ export function EachBlock(node, context) {
const child_state = {
...context.state,
transform: { ...context.state.transform }
transform: { ...context.state.transform },
store_to_invalidate
};
/** The state used when generating the key function, if necessary */

@ -48,7 +48,9 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
(trimmed[0].type === 'SvelteFragment' ||
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' && trimmed[0].elseif));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Identifier } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
@ -19,29 +19,28 @@ export function IfBlock(node, context) {
let alternate_id;
if (node.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
alternate_id = context.state.scope.generate('alternate');
statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate)));
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
const nodes = node.alternate.nodes;
let alternate_args = [b.id('$$anchor')];
if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
alternate_args.push(b.id('$$elseif'));
}
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
/** @type {Expression[]} */
const args = [
context.state.node,
node.elseif ? b.id('$$anchor') : context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id
? b.stmt(
b.call(
b.id('$$render'),
b.id(alternate_id),
node.alternate ? b.literal(false) : undefined
)
)
: undefined
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)
])
)
@ -69,7 +68,7 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.literal(true));
args.push(b.id('$$elseif'));
}
statements.push(b.stmt(b.call('$.if', ...args)));

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -20,16 +20,17 @@ import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_style_directives,
build_set_attributes
build_set_attributes,
build_set_class,
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
@ -214,21 +215,17 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
// Then do attributes
let is_attributes_reactive = has_spread;
if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
build_set_attributes(
attributes,
class_directives,
style_directives,
context,
node,
node_id,
attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
is_custom_element_node(node) && b.true,
context.state
attributes_id
);
// If value binding exists, that one takes care of calling $.init_select
@ -269,13 +266,24 @@ export function RegularElement(node, context) {
continue;
}
const name = get_attribute_name(node, attribute);
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
(attribute.value === true || is_text_attribute(attribute))
(attribute.value === true || is_text_attribute(attribute)) &&
(name !== 'class' || class_directives.length === 0) &&
(name !== 'style' || style_directives.length === 0)
) {
const name = get_attribute_name(node, attribute);
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) {
if (value === true || value === '') {
value = context.state.analysis.css.hash;
} else {
value += ' ' + context.state.analysis.css.hash;
}
}
if (name !== 'class' || value) {
context.state.template.push(
@ -286,19 +294,29 @@ export function RegularElement(node, context) {
}`
);
}
continue;
}
} else if (name === 'autofocus') {
let { value } = build_attribute_value(attribute.value, context);
context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
} else if (name === 'class') {
const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg';
build_set_class(node, node_id, attribute, class_directives, context, is_html);
} else if (name === 'style') {
build_set_style(node_id, attribute, style_directives, context);
} else if (is_custom_element) {
build_custom_element_attribute_update_assignment(node_id, attribute, context);
} else {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
const is = is_custom_element
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
if (is) is_attributes_reactive = true;
(has_state ? context.state.update : context.state.init).push(b.stmt(update));
}
}
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
if (
is_load_error_element(node.name) &&
@ -363,15 +381,14 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
const { value } = build_template_chunk(trimmed, context.visit, child_state);
const empty_string = value.type === 'Literal' && value.value === '';
if (!empty_string) {
child_state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_chunk(trimmed, context.visit, child_state).value
)
)
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
);
}
} else {
/** @type {Expression} */
let arg = context.state.node;
@ -491,6 +508,56 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, context) {
let properties = [];
let has_call_or_state = false;
for (const d of class_directives) {
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
}
const directives = b.object(properties);
return has_call_or_state ? get_expression_id(context.state, directives) : directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
*/
export function build_style_directives_object(style_directives, context) {
let normal_properties = [];
let important_properties = [];
for (const directive of style_directives) {
const expression =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
).value;
const property = b.init(directive.name, expression);
if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {
normal_properties.push(property);
}
}
return important_properties.length
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties);
}
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@ -515,63 +582,29 @@ function setup_select_synchronization(value_binding, context) {
* Returns true if attribute is deemed reactive, false otherwise.
* @param {AST.RegularElement} element
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {string} name
* @param {Expression} value
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @returns {boolean}
*/
function build_element_attribute_update_assignment(
element,
node_id,
attribute,
attributes,
context
) {
const state = context.state;
const name = get_attribute_name(element, attribute);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(state, value) : value
);
if (name === 'autofocus') {
state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
return false;
function build_element_attribute_update(element, node_id, name, value, attributes) {
if (name === 'muted') {
// Special case for Firefox who needs it set as a property in order to work
return b.assignment('=', b.member(node_id, b.id('muted')), value);
}
// Special case for Firefox who needs it set as a property in order to work
if (name === 'muted') {
state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
return false;
if (name === 'value') {
return b.call('$.set_value', node_id, value);
}
/** @type {Statement} */
let update;
if (name === 'checked') {
return b.call('$.set_checked', node_id, value);
}
if (name === 'class') {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
if (name === 'selected') {
return b.call('$.set_selected', node_id, value);
}
update = b.stmt(
b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id,
value,
attribute.metadata.needs_clsx && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: undefined
)
);
} else if (name === 'value') {
update = b.stmt(b.call('$.set_value', node_id, value));
} else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value));
} else if (name === 'selected') {
update = b.stmt(b.call('$.set_selected', node_id, value));
} else if (
if (
// If we would just set the defaultValue property, it would override the value property,
// because it is set in the template which implicitly means it's also setting the default value,
// and if one updates the default value while the input is pristine it will also update the
@ -582,70 +615,49 @@ function build_element_attribute_update_assignment(
) ||
(element.name === 'textarea' && element.fragment.nodes.length > 0))
) {
update = b.stmt(b.call('$.set_default_value', node_id, value));
} else if (
return b.call('$.set_default_value', node_id, value);
}
if (
// See defaultValue comment
name === 'defaultChecked' &&
attributes.some(
(attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
)
) {
update = b.stmt(b.call('$.set_default_checked', node_id, value));
} else if (is_dom_property(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else {
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
update = b.stmt(
b.call(
callee,
return b.call('$.set_default_checked', node_id, value);
}
if (is_dom_property(name)) {
return b.assignment('=', b.member(node_id, name), value);
}
return b.call(
name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute',
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
}
if (has_state) {
state.update.push(update);
return true;
} else {
state.init.push(update);
return false;
}
}
/**
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.
* Like `build_element_attribute_update` but without any special attribute treatment.
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
const state = context.state;
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let { value, has_state } = build_attribute_value(attribute.value, context);
// We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
if (name === 'class' && attribute.metadata.needs_clsx) {
if (context.state.analysis.css.hash) {
value = b.array([value, b.literal(context.state.analysis.css.hash)]);
}
value = b.call('$.clsx', value);
}
const { value, has_state } = build_attribute_value(attribute.value, context);
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
// don't lowercase name, as we set the element's property, which might be case sensitive
const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value);
if (has_state) {
// this is different from other updates — it doesn't get grouped,
// because set_custom_element_data may not be idempotent
state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression))));
return true;
} else {
state.init.push(update);
return false;
}
const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call;
context.state.init.push(b.stmt(update));
}
/**
@ -656,29 +668,33 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(state, value) : value
metadata.has_call
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(state, value)
: get_expression_id(state, value)
: value
);
const inner_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
b.conditional(
b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)),
b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)),
b.literal(''), // render null/undefined values as empty string to support placeholder options
value
)
);
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const update = b.stmt(
is_select_with_value
? b.sequence([
@ -708,9 +724,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
value,
update
);
return true;
} else {
state.init.push(update);
return false;
}
}

@ -59,7 +59,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(

@ -1,6 +1,7 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
@ -35,6 +36,9 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const external_statements = [];
/** @type {Statement[]} */
const internal_statements = [];
const snippets_visits = [];
// Capture the `failed` implicit snippet prop
@ -53,7 +57,20 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
if (dev) {
// In dev we must separate the declarations from the code
// that eagerly evaluate the expression...
for (const statement of init) {
if (statement.type === 'VariableDeclaration') {
external_statements.push(statement);
} else {
internal_statements.push(statement);
}
}
} else {
external_statements.push(...init);
}
} else {
nodes.push(child);
}
@ -63,6 +80,10 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
if (dev && internal_statements.length) {
block.body.unshift(...internal_statements);
}
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);

@ -5,13 +5,8 @@ import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
build_class_directives,
build_set_attributes,
build_style_directives
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
import { build_render_statement, get_expression_id } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -77,36 +72,29 @@ export function SvelteElement(node, context) {
// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
// Then do attributes
let is_attributes_reactive = false;
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
inner_context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
} else {
if (
attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
attributes[0].name.toLowerCase() === 'class' &&
is_text_attribute(attributes[0])
) {
build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
} else if (attributes.length) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
is_attributes_reactive = build_set_attributes(
build_set_attributes(
attributes,
class_directives,
style_directives,
inner_context,
node,
element_id,
attributes_id,
b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')),
context.state
attributes_id
);
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
if (dev) {

@ -116,8 +116,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
let options =
args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined;

@ -1,32 +1,31 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter } from '../../utils.js';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {Identifier} attributes_id
* @param {false | Expression} preserve_attribute_case
* @param {false | Expression} is_custom_element
* @param {ComponentClientTransformState} state
*/
export function build_set_attributes(
attributes,
class_directives,
style_directives,
context,
element,
element_id,
attributes_id,
preserve_attribute_case,
is_custom_element,
state
attributes_id
) {
let is_dynamic = false;
@ -68,102 +67,48 @@ export function build_set_attributes(
}
}
const call = b.call(
'$.set_attributes',
element_id,
is_dynamic ? attributes_id : b.literal(null),
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
preserve_attribute_case,
is_custom_element,
is_ignored(element, 'hydration_attribute_changed') && b.true
if (class_directives.length) {
values.push(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context)
)
);
if (is_dynamic) {
context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
return true;
is_dynamic ||=
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
}
context.state.init.push(b.stmt(call));
return false;
}
/**
* Serializes each style directive into something like `$.set_style(element, style_property, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {AST.StyleDirective[]} style_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
export function build_style_directives(
style_directives,
element_id,
context,
is_attributes_reactive
) {
const state = context.state;
for (const directive of style_directives) {
const { has_state } = directive.metadata.expression;
let value =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
).value;
const update = b.stmt(
b.call(
'$.set_style',
element_id,
b.literal(directive.name),
value,
/** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined)
if (style_directives.length) {
values.push(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, context)
)
);
if (has_state || is_attributes_reactive) {
state.update.push(update);
} else {
state.init.push(update);
}
is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
}
}
/**
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {AST.ClassDirective[]} class_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
export function build_class_directives(
class_directives,
const call = b.call(
'$.set_attributes',
element_id,
context,
is_attributes_reactive
) {
const state = context.state;
for (const directive of class_directives) {
const { has_state, has_call } = directive.metadata.expression;
let value = /** @type {Expression} */ (context.visit(directive.expression));
if (has_call) {
value = get_expression_id(state, value);
}
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
is_dynamic ? attributes_id : b.null,
b.object(values),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
);
if (is_attributes_reactive || has_state) {
state.update.push(update);
if (is_dynamic) {
context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
} else {
state.init.push(update);
}
context.state.init.push(b.stmt(call));
}
}
@ -175,7 +120,7 @@ export function build_class_directives(
*/
export function build_attribute_value(value, context, memoize = (value) => value) {
if (value === true) {
return { value: b.literal(true), has_state: false };
return { value: b.true, has_state: false };
}
if (!Array.isArray(value) || value.length === 1) {
@ -207,3 +152,120 @@ export function get_attribute_name(element, attribute) {
return attribute.name;
}
/**
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {boolean} is_html
*/
export function build_set_class(element, node_id, attribute, class_directives, context, is_html) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
return metadata.has_call ? get_expression_id(context.state, value) : value;
});
/** @type {Identifier | undefined} */
let previous_id;
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ObjectExpression | Identifier | undefined} */
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
previous_id = b.id(context.state.scope.generate('classes'));
context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
prev = previous_id;
} else {
prev = b.object([]);
}
}
/** @type {Expression | undefined} */
let css_hash;
if (element.metadata.scoped && context.state.analysis.css.hash) {
if (value.type === 'Literal' && (value.value === '' || value.value === null)) {
value = b.literal(context.state.analysis.css.hash);
} else if (value.type === 'Literal' && typeof value.value === 'string') {
value = b.literal(escape_html(value.value, true) + ' ' + context.state.analysis.css.hash);
} else {
css_hash = b.literal(context.state.analysis.css.hash);
}
}
if (!css_hash && next) {
css_hash = b.null;
}
/** @type {Expression} */
let set_class = b.call(
'$.set_class',
node_id,
is_html ? b.literal(1) : b.literal(0),
value,
css_hash,
prev,
next
);
if (previous_id) {
set_class = b.assignment('=', previous_id, set_class);
}
(has_state ? context.state.update : context.state.init).push(b.stmt(set_class));
}
/**
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
);
/** @type {Identifier | undefined} */
let previous_id;
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ArrayExpression | ObjectExpression | undefined} */
let next;
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
previous_id = b.id(context.state.scope.generate('styles'));
context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
prev = previous_id;
} else {
prev = b.object([]);
}
}
/** @type {Expression} */
let set_style = b.call('$.set_style', node_id, value, prev, next);
if (previous_id) {
set_style = b.assignment('=', previous_id, set_style);
}
(has_state ? context.state.update : context.state.init).push(b.stmt(set_style));
}

@ -46,11 +46,15 @@ export function visit_event_attribute(node, context) {
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
@ -123,13 +127,21 @@ export function build_event_handler(node, metadata, context) {
}
// function declared in the script
if (
handler.type === 'Identifier' &&
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
) {
if (handler.type === 'Identifier') {
const binding = context.state.scope.get(handler.name);
if (binding?.is_function()) {
return handler;
}
// local variable can be assigned directly
// except in dev mode where when need $.apply()
// in order to handle warnings.
if (!dev && binding?.declaration_kind !== 'import') {
return handler;
}
}
if (metadata.has_call) {
// memoize where necessary
const id = b.id(context.state.scope.generate('event_handler'));

@ -144,6 +144,17 @@ function is_static_element(node, state) {
return false;
}
if (attribute.name === 'dir') {
return false;
}
if (
['input', 'textarea'].includes(node.name) &&
['value', 'checked'].includes(attribute.name)
) {
return false;
}
if (node.name === 'option' && attribute.name === 'value') {
return false;
}

@ -26,55 +26,9 @@ export function memoize_expression(state, value) {
* @param {Expression} value
*/
export function get_expression_id(state, value) {
for (let i = 0; i < state.expressions.length; i += 1) {
if (compare_expressions(state.expressions[i], value)) {
return b.id(`$${i}`);
}
}
return b.id(`$${state.expressions.push(value) - 1}`);
}
/**
* Returns true of two expressions have an identical AST shape
* @param {Expression} a
* @param {Expression} b
*/
function compare_expressions(a, b) {
if (a.type !== b.type) {
return false;
}
for (const key in a) {
if (key === 'type' || key === 'metadata' || key === 'loc' || key === 'start' || key === 'end') {
continue;
}
const va = /** @type {any} */ (a)[key];
const vb = /** @type {any} */ (b)[key];
if ((typeof va === 'object') !== (typeof vb === 'object')) {
return false;
}
if (typeof va !== 'object' || va === null || vb === null) {
if (va !== vb) return false;
} else if (Array.isArray(va)) {
if (va.length !== vb.length) {
return false;
}
if (va.some((v, i) => !compare_expressions(v, vb[i]))) {
return false;
}
} else if (!compare_expressions(va, vb)) {
return false;
}
}
return true;
}
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
@ -101,11 +55,15 @@ export function build_template_chunk(
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
} else if (node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
}
} else {
} else if (
node.expression.type !== 'Identifier' ||
node.expression.name !== 'undefined' ||
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
node.metadata.expression
@ -117,8 +75,8 @@ export function build_template_chunk(
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
} else {
// add `?? ''` where necessary (TODO optimise more cases)
}
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
@ -129,18 +87,20 @@ export function build_template_chunk(
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} else if (
state.analysis.props_id &&
value.type === 'Identifier' &&
value.name === state.analysis.props_id.name
) {
// do nothing ($props.id() is never null/undefined)
} else {
}
const is_defined =
value.type === 'BinaryExpression' ||
(value.type === 'UnaryExpression' && value.operator !== 'void') ||
(value.type === 'LogicalExpression' && value.right.type === 'Literal') ||
(value.type === 'Identifier' && value.name === state.analysis.props_id?.name);
if (!is_defined) {
// add `?? ''` where necessary (TODO optimise more cases)
value = b.logical('??', value, b.literal(''));
}
expressions.push(value);
}
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
@ -151,7 +111,10 @@ export function build_template_chunk(
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
const value = b.template(quasis, expressions);
const value =
expressions.length > 0
? b.template(quasis, expressions)
: b.literal(/** @type {string} */ (quasi.value.cooked));
return { value, has_state };
}
@ -309,12 +272,16 @@ export function validate_binding(state, binding, expression) {
const loc = locator(binding.start);
const obj = /** @type {Expression} */ (expression.object);
state.init.push(
b.stmt(
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
b.thunk(/** @type {Expression} */ (expression.object)),
b.thunk(
state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
),
b.thunk(
/** @type {Expression} */ (
expression.computed

@ -13,11 +13,11 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('undefined');
return b.void0;
}
if (rune === '$effect.tracking') {
return b.literal(false);
return b.false;
}
if (rune === '$effect.root') {

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
@ -10,19 +10,29 @@ import { block_close, block_open } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
context.state.template.push(if_statement, block_close);
let index = 1;
let alt = node.alternate;
while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent));
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(`<!--[${index++}-->`)))
);
if_statement = if_statement.alternate = b.if(
/** @type {Expression} */ (context.visit(elseif.test)),
alternate
);
alt = elseif.alternate;
}
context.state.template.push(b.if(test, consequent, alternate), block_close);
if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
if_statement.alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
}

@ -38,7 +38,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
? b.null
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(

@ -45,7 +45,7 @@ export function VariableDeclaration(node, context) {
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
: b.id('undefined');
: b.void0;
return b.assignment_pattern(node.left, right);
}
}
@ -75,8 +75,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
if (rune === '$derived.by') {
declarations.push(

@ -1,4 +1,4 @@
/** @import { Expression, Literal } from 'estree' */
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import {
@ -24,6 +24,7 @@ import {
is_content_editable_binding,
is_load_error_element
} from '../../../../../../utils.js';
import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
@ -47,9 +48,6 @@ export function build_element_attributes(node, context) {
let content = null;
let has_spread = false;
// Use the index to keep the attributes order which is important for spreading
let class_index = -1;
let style_index = -1;
let events_to_capture = new Set();
for (const attribute of node.attributes) {
@ -85,34 +83,21 @@ export function build_element_attributes(node, context) {
// the defaultValue/defaultChecked properties don't exist as attributes
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
class_index = attributes.length;
if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash
? b.binary(
'+',
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
expression: b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
)
: clsx_value
}
});
} else {
attributes.push(attribute);
}
} else {
if (attribute.name === 'style') {
style_index = attributes.length;
}
attributes.push(attribute);
}
}
@ -219,40 +204,30 @@ export function build_element_attributes(node, context) {
}
}
if (class_directives.length > 0 && !has_spread) {
const class_attribute = build_class_directives(
class_directives,
/** @type {AST.Attribute | null} */ (attributes[class_index] ?? null)
);
if (class_index === -1) {
attributes.push(class_attribute);
}
}
if (style_directives.length > 0 && !has_spread) {
build_style_directives(
style_directives,
/** @type {AST.Attribute | null} */ (attributes[style_index] ?? null),
context
);
if (style_index > -1) {
attributes.splice(style_index, 1);
}
}
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
} else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.value === true || is_text_attribute(attribute)) {
const name = get_attribute_name(node, attribute);
const literal_value = /** @type {Literal} */ (
const can_use_literal =
(name !== 'class' || class_directives.length === 0) &&
(name !== 'style' || style_directives.length === 0);
if (can_use_literal && (attribute.value === true || is_text_attribute(attribute))) {
let literal_value = /** @type {Literal} */ (
build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
)
).value;
if (name === 'class' && css_hash) {
literal_value = (String(literal_value) + ' ' + css_hash).trim();
}
if (name !== 'class' || literal_value) {
context.state.template.push(
b.literal(
@ -264,21 +239,33 @@ export function build_element_attributes(node, context) {
)
);
}
continue;
}
const name = get_attribute_name(node, attribute);
const value = build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
// pre-escape and inline literal attributes :
if (can_use_literal && value.type === 'Literal' && typeof value.value === 'string') {
if (name === 'class' && css_hash) {
value.value = (value.value + ' ' + css_hash).trim();
}
context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') {
context.state.template.push(build_attr_class(class_directives, value, context, css_hash));
} else if (name === 'style') {
context.state.template.push(build_attr_style(style_directives, value, context));
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
}
}
if (events_to_capture.size !== 0) {
for (const event of events_to_capture) {
@ -322,7 +309,7 @@ function build_element_spread_attributes(
let styles;
let flags = 0;
if (class_directives.length > 0 || context.state.analysis.css.hash) {
if (class_directives.length) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
@ -331,11 +318,6 @@ function build_element_spread_attributes(
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
if (context.state.analysis.css.hash) {
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
}
classes = b.object(properties);
}
@ -374,83 +356,90 @@ function build_element_spread_attributes(
})
);
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
const css_hash =
element.metadata.scoped && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: b.null;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
}
/**
*
* @param {AST.ClassDirective[]} class_directives
* @param {AST.Attribute | null} class_attribute
* @returns
* @param {Expression} expression
* @param {ComponentContext} context
* @param {string | null} hash
*/
function build_class_directives(class_directives, class_attribute) {
const expressions = class_directives.map((directive) =>
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
function build_attr_class(class_directives, expression, context, hash) {
/** @type {ObjectExpression | undefined} */
let directives;
if (class_directives.length) {
directives = b.object(
class_directives.map((directive) =>
b.prop(
'init',
b.literal(directive.name),
/** @type {Expression} */ (context.visit(directive.expression, context.state))
)
)
);
if (class_attribute === null) {
class_attribute = create_attribute('class', -1, -1, []);
}
const chunks = get_attribute_chunks(class_attribute.value);
const last = chunks.at(-1);
let css_hash;
if (last?.type === 'Text') {
last.data += ' ';
last.raw += ' ';
} else if (last) {
chunks.push({
type: 'Text',
start: -1,
end: -1,
data: ' ',
raw: ' '
});
if (hash) {
if (expression.type === 'Literal' && typeof expression.value === 'string') {
expression.value = (expression.value + ' ' + hash).trim();
} else {
css_hash = b.literal(hash);
}
chunks.push({
type: 'ExpressionTag',
start: -1,
end: -1,
expression: b.call(
b.member(b.call(b.member(b.array(expressions), 'filter'), b.id('Boolean')), b.id('join')),
b.literal(' ')
),
metadata: {
expression: create_expression_metadata()
}
});
class_attribute.value = chunks;
return class_attribute;
return b.call('$.attr_class', expression, css_hash, directives);
}
/**
*
* @param {AST.StyleDirective[]} style_directives
* @param {AST.Attribute | null} style_attribute
* @param {Expression} expression
* @param {ComponentContext} context
*/
function build_style_directives(style_directives, style_attribute, context) {
const styles = style_directives.map((directive) => {
let value =
function build_attr_style(style_directives, expression, context) {
/** @type {ArrayExpression | ObjectExpression | undefined} */
let directives;
if (style_directives.length) {
let normal_properties = [];
let important_properties = [];
for (const directive of style_directives) {
const expression =
directive.value === true
? b.id(directive.name)
: build_attribute_value(directive.value, context, true);
let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') {
name = name.toLowerCase();
}
const property = b.init(directive.name, expression);
if (directive.modifiers.includes('important')) {
value = b.binary('+', value, b.literal(' !important'));
important_properties.push(property);
} else {
normal_properties.push(property);
}
}
return b.init(directive.name, value);
});
const arg =
style_attribute === null
? b.object(styles)
: b.call(
'$.merge_styles',
build_attribute_value(style_attribute.value, context, true),
b.object(styles)
);
if (important_properties.length) {
directives = b.array([b.object(normal_properties), b.object(important_properties)]);
} else {
directives = b.object(normal_properties);
}
}
context.state.template.push(b.call('$.add_styles', arg));
return b.call('$.attr_style', expression, directives);
}

@ -1,6 +1,6 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, Binding, DeclarationKind } from '#compiler' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
@ -16,6 +16,90 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
export class Binding {
/** @type {Scope} */
scope;
/** @type {Identifier} */
node;
/** @type {BindingKind} */
kind;
/** @type {DeclarationKind} */
declaration_kind;
/**
* What the value was initialized with.
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
* @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
*/
initial;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/**
* For `legacy_reactive`: its reactive dependencies
* @type {Binding[]}
*/
legacy_dependencies = [];
/**
* Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
* @type {string | null}
*/
prop_alias = null;
/**
* Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean }}
*/
metadata = null;
mutated = false;
reassigned = false;
/**
*
* @param {Scope} scope
* @param {Identifier} node
* @param {BindingKind} kind
* @param {DeclarationKind} declaration_kind
* @param {Binding['initial']} initial
*/
constructor(scope, node, kind, declaration_kind, initial) {
this.scope = scope;
this.node = node;
this.initial = initial;
this.kind = kind;
this.declaration_kind = declaration_kind;
}
get updated() {
return this.mutated || this.reassigned;
}
/**
* @returns {this is Binding & { initial: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression }}
*/
is_function() {
if (this.updated) {
// even if it's reassigned to another function,
// we can't use it directly as e.g. an event handler
return false;
}
const type = this.initial?.type;
return (
type === 'ArrowFunctionExpression' ||
type === 'FunctionExpression' ||
type === 'FunctionDeclaration'
);
}
}
export class Scope {
/** @type {ScopeRoot} */
root;
@ -96,26 +180,15 @@ export class Scope {
}
if (this.declarations.has(node.name)) {
// This also errors on var/function types, but that's arguably a good thing
const binding = this.declarations.get(node.name);
if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') {
// This also errors on function types, but that's arguably a good thing
// declaring function twice is also caught by acorn in the parse phase
e.declaration_duplicate(node, node.name);
}
}
/** @type {Binding} */
const binding = {
node,
references: [],
legacy_dependencies: [],
initial,
reassigned: false,
mutated: false,
updated: false,
scope: this,
kind,
declaration_kind,
is_called: false,
prop_alias: null,
metadata: null
};
const binding = new Binding(this, node, kind, declaration_kind, initial);
validate_identifier_name(binding, this.function_depth);
@ -690,8 +763,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const binding = left && scope.get(left.name);
if (binding !== null && left !== binding.node) {
binding.updated = true;
if (left === expression) {
binding.reassigned = true;
} else {

@ -1,12 +1,5 @@
import type {
ClassDeclaration,
Expression,
FunctionDeclaration,
Identifier,
ImportDeclaration
} from 'estree';
import type { SourceMap } from 'magic-string';
import type { Scope } from '../phases/scope.js';
import type { Binding } from '../phases/scope.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
@ -241,6 +234,20 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
hmr: CompileOptions['hmr'];
};
export type BindingKind =
| 'normal' // A variable that is not in any way special
| 'prop' // A normal prop (possibly reassigned or mutated)
| 'bindable_prop' // A prop one can `bind:` to (possibly reassigned or mutated)
| 'rest_prop' // A rest prop
| 'raw_state' // A state variable
| 'state' // A deeply reactive state variable
| 'derived' // A derived variable
| 'each' // An each block parameter
| 'snippet' // A snippet parameter
| 'store_sub' // A $store value
| 'legacy_reactive' // A `$:` declaration
| 'template'; // A binding declared in the template, e.g. in an `await` block or `const` tag
export type DeclarationKind =
| 'var'
| 'let'
@ -251,66 +258,6 @@ export type DeclarationKind =
| 'rest_param'
| 'synthetic';
export interface Binding {
node: Identifier;
/**
* - `normal`: A variable that is not in any way special
* - `prop`: A normal prop (possibly reassigned or mutated)
* - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated)
* - `rest_prop`: A rest prop
* - `state`: A state variable
* - `derived`: A derived variable
* - `each`: An each block parameter
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `template`: A binding declared in the template, e.g. in an `await` block or `const` tag
*/
kind:
| 'normal'
| 'prop'
| 'bindable_prop'
| 'rest_prop'
| 'state'
| 'raw_state'
| 'derived'
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'template'
| 'snippet';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
*/
initial:
| null
| Expression
| FunctionDeclaration
| ClassDeclaration
| ImportDeclaration
| AST.EachBlock
| AST.SnippetBlock;
is_called: boolean;
references: { node: Identifier; path: AST.SvelteNode[] }[];
mutated: boolean;
reassigned: boolean;
/** `true` if mutated _or_ reassigned */
updated: boolean;
scope: Scope;
/** For `legacy_reactive`: its reactive dependencies */
legacy_dependencies: Binding[];
/** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */
prop_alias: string | null;
/** Additional metadata, varies per binding type */
metadata: {
/** `true` if is (inside) a rest parameter */
inside_rest?: boolean;
} | null;
}
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
dependencies: Set<Binding>;
@ -322,5 +269,7 @@ export interface ExpressionMetadata {
export * from './template.js';
export { Binding, Scope } from '../phases/scope.js';
// TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js';

@ -154,6 +154,8 @@ export function unary(operator, argument) {
return { type: 'UnaryExpression', argument, operator, prefix: true };
}
export const void0 = unary('void', literal(0));
/**
* @param {ESTree.Expression} test
* @param {ESTree.Expression} consequent
@ -483,7 +485,7 @@ export function do_while(test, body) {
const true_instance = literal(true);
const false_instance = literal(false);
const null_instane = literal(null);
const null_instance = literal(null);
/** @type {ESTree.DebuggerStatement} */
const debugger_builder = {
@ -645,7 +647,7 @@ export {
return_builder as return,
if_builder as if,
this_instance as this,
null_instane as null,
null_instance as null,
debugger_builder as debugger
};

@ -33,6 +33,7 @@ export const UNINITIALIZED = Symbol();
export const FILENAME = Symbol('filename');
export const HMR = Symbol('hmr');
export const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
export const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { flush_sync, untrack } from './internal/client/runtime.js';
import { untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@ -45,13 +45,14 @@ if (DEV) {
}
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
* it can be called from an external module).
* `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
* Unlike `$effect`, the provided function only runs once.
*
* If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted.
* It must be called during the component's initialisation (but doesn't need to live _inside_ the component;
* it can be called from an external module). If a function is returned _synchronously_ from `onMount`,
* it will be called when the component is unmounted.
*
* `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render).
* `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render).
*
* @template T
* @param {() => NotFunction<T> | Promise<NotFunction<T>> | (() => any)} fn
@ -206,15 +207,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
/**
* Synchronously flushes any pending state changes and those that result from it.
* @param {() => void} [fn]
* @returns {void}
*/
export function flushSync(fn) {
flush_sync(fn);
}
export { flushSync } from './internal/client/runtime.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js';

@ -19,6 +19,7 @@ export interface ComponentConstructorOptions<
intro?: boolean;
recover?: boolean;
sync?: boolean;
idPrefix?: string;
$$inline?: boolean;
}

@ -11,7 +11,7 @@ import {
set_active_reaction,
untrack
} from './runtime.js';
import { effect } from './reactivity/effects.js';
import { effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
/** @type {ComponentContext | null} */
@ -112,15 +112,16 @@ export function getAllContexts() {
* @returns {void}
*/
export function push(props, runes = false, fn) {
component_context = {
var ctx = (component_context = {
p: component_context,
c: null,
d: false,
e: null,
m: false,
s: props,
x: null,
l: null
};
});
if (legacy_mode_flag && !runes) {
component_context.l = {
@ -131,6 +132,10 @@ export function push(props, runes = false, fn) {
};
}
teardown(() => {
/** @type {ComponentContext} */ (ctx).d = true;
});
if (DEV) {
// component function
component_context.function = fn;

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flush_sync, 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 { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
@ -105,7 +105,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
flushSync();
}
}
}

@ -9,16 +9,16 @@ import {
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn
* @param {[number,number]} [elseif]
* @returns {void}
*/
export function if_block(node, fn, elseif = false) {
if (hydrating) {
export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && root_index === 0) {
hydrate_next();
}
@ -33,26 +33,44 @@ export function if_block(node, fn, elseif = false) {
/** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0;
var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0;
var has_branch = false;
const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
const set_branch = (
/** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn,
flag = true
) => {
has_branch = true;
update_branch(flag, fn);
};
const update_branch = (
/** @type {boolean | null} */ new_condition,
/** @type {null | ((anchor: Node) => void)} */ fn
/** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn
) => {
if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
if (hydrating) {
const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
if (hydrating && hydrate_index !== -1) {
if (root_index === 0) {
const data = /** @type {Comment} */ (anchor).data;
if (data === HYDRATION_START) {
hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) {
hydrate_index = Infinity;
} else {
hydrate_index = parseInt(data.substring(1));
if (hydrate_index !== hydrate_index) {
// if hydrate_index is NaN
// we set an invalid index to force mismatch
hydrate_index = condition ? Infinity : -1;
}
}
}
const is_else = hydrate_index > root_index;
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
@ -62,6 +80,7 @@ export function if_block(node, fn, elseif = false) {
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
hydrate_index = -1; // ignore hydration in next else if
}
}
@ -81,7 +100,7 @@ export function if_block(node, fn, elseif = false) {
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (fn) {
alternate_effect = branch(() => fn(anchor));
alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index]));
}
if (consequent_effect) {

@ -14,6 +14,15 @@ import {
set_active_reaction
} from '../../runtime.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { NAMESPACE_HTML } from '../../../../constants.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
const IS_CUSTOM_ELEMENT = Symbol('is custom element');
const IS_HTML = Symbol('is html');
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -59,8 +68,7 @@ export function remove_input_defaults(input) {
* @param {any} value
*/
export function set_value(element, value) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
var attributes = get_attributes(element);
if (
attributes.value ===
@ -83,8 +91,7 @@ export function set_value(element, value) {
* @param {boolean} checked
*/
export function set_checked(element, checked) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
var attributes = get_attributes(element);
if (
attributes.checked ===
@ -147,8 +154,7 @@ export function set_default_value(element, value) {
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
var attributes = get_attributes(element);
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
@ -172,11 +178,6 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (attributes[attribute] === (attributes[attribute] = value)) return;
if (attribute === 'style' && '__styles' in element) {
// reset styles to force style: directive to update
element.__styles = {};
}
if (attribute === 'loading') {
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = value;
@ -213,6 +214,7 @@ export function set_custom_element_data(node, prop, value) {
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let was_hydrating = hydrating;
@ -222,17 +224,20 @@ export function set_custom_element_data(node, prop, value) {
set_active_reaction(null);
set_active_effect(null);
try {
if (
// `style` should use `set_attribute` rather than the setter
prop !== 'style' &&
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
setters_cache.has(node.nodeName) ||
(setters_cache.has(node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object'
: value && typeof value === 'object')
) {
// @ts-expect-error
node[prop] = value;
@ -254,23 +259,18 @@ export function set_custom_element_data(node, prop, value) {
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
* @param {Record<string, any> | undefined} prev
* @param {Record<string, any>} next New attributes - this function mutates this object
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [preserve_attribute_case]
* @param {boolean} [is_custom_element]
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
export function set_attributes(
element,
prev,
next,
css_hash,
preserve_attribute_case = false,
is_custom_element = false,
skip_warning = false
) {
export function set_attributes(element, prev, next, css_hash, skip_warning = false) {
var attributes = get_attributes(element);
var is_custom_element = attributes[IS_CUSTOM_ELEMENT];
var preserve_attribute_case = !attributes[IS_HTML];
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let is_hydrating_custom_element = hydrating && is_custom_element;
@ -289,17 +289,16 @@ export function set_attributes(
if (next.class) {
next.class = clsx(next.class);
} else if (css_hash || next[CLASS]) {
next.class = null; /* force call to set_class() */
}
if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
if (next[STYLE]) {
next.style ??= null; /* force call to set_style() */
}
var setters = get_setters(element);
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
// since key is captured we use const
for (const key in next) {
// let instead of var because referenced in a closure
@ -324,6 +323,21 @@ export function set_attributes(
continue;
}
if (key === 'class') {
var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]);
current[key] = value;
current[CLASS] = next[CLASS];
continue;
}
if (key === 'style') {
set_style(element, value, prev?.[STYLE], next[STYLE]);
current[key] = value;
current[STYLE] = next[STYLE];
continue;
}
var prev_value = current[key];
if (value === prev_value) continue;
@ -375,8 +389,9 @@ export function set_attributes(
// @ts-ignore
element[`__${event_name}`] = undefined;
}
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
} else if (key === 'style') {
// avoid using the setter
set_attribute(element, key, value);
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
@ -422,13 +437,9 @@ export function set_attributes(
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
set_attribute(element, name, value);
set_attribute(element, name, value, skip_warning);
}
}
if (key === 'style' && '__styles' in element) {
// reset styles to force style: directive to update
element.__styles = {};
}
}
if (is_hydrating_custom_element) {
@ -438,6 +449,20 @@ export function set_attributes(
return current;
}
/**
*
* @param {Element} element
*/
function get_attributes(element) {
return /** @type {Record<string | symbol, unknown>} **/ (
// @ts-expect-error
element.__attributes ??= {
[IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
);
}
/** @type {Map<string, string[]>} */
var setters_cache = new Map();

@ -1,120 +1,47 @@
import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {SVGElement} dom
* @param {string} value
* @param {Element} dom
* @param {boolean | number} is_html
* @param {string | null} value
* @param {string} [hash]
* @returns {void}
* @param {Record<string, any>} [prev_classes]
* @param {Record<string, any>} [next_classes]
* @returns {Record<string, boolean> | undefined}
*/
export function set_svg_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
var prev = dom.__className;
/**
* @param {MathMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_mathml_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating || prev !== value) {
var next_class_name = to_class(value, hash, next_classes);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
} else if (is_html) {
dom.className = next_class_name;
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {HTMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
dom.__className = value;
} else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name)
) {
// Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null && !hash) {
dom.removeAttribute('class');
} else {
dom.className = next_class_name;
if (prev_classes == null || is_present !== !!prev_classes[key]) {
dom.classList.toggle(key, is_present);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @template V
* @param {V} value
* @param {string} [hash]
* @returns {string | V}
*/
function to_class(value, hash) {
return (value == null ? '' : value) + (hash ? ' ' + hash : '');
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {boolean} value
* @returns {void}
*/
export function toggle_class(dom, class_name, value) {
if (value) {
if (dom.classList.contains(class_name)) return;
dom.classList.add(class_name);
} else {
if (!dom.classList.contains(class_name)) return;
dom.classList.remove(class_name);
}
return next_classes;
}

@ -237,7 +237,13 @@ export function handle_event_propagation(event) {
// @ts-expect-error
var delegated = current_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (
delegated != null &&
(!(/** @type {any} */ (current_target).disabled) ||
// DOM could've been updated already by the time this is reached, so we check this as well
// -> the target could not have been disabled because it emits the event in the first place
event.target === current_target)
) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
@ -305,13 +311,11 @@ export function apply(
error = e;
}
if (typeof handler === 'function') {
handler.apply(element, args);
} else if (has_side_effects || handler != null || error) {
if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const event_name = args[0].type;
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
@ -321,4 +325,5 @@ export function apply(
throw error;
}
}
handler?.apply(element, args);
}

@ -1,22 +1,57 @@
import { to_style } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {HTMLElement} dom
* @param {string} key
* @param {string} value
* @param {boolean} [important]
* @param {Element & ElementCSSInlineStyle} dom
* @param {Record<string, any>} prev
* @param {Record<string, any>} next
* @param {string} [priority]
*/
export function set_style(dom, key, value, important) {
// @ts-expect-error
var styles = (dom.__styles ??= {});
function update_styles(dom, prev = {}, next, priority) {
for (var key in next) {
var value = next[key];
if (styles[key] === value) {
return;
if (prev[key] !== value) {
if (next[key] == null) {
dom.style.removeProperty(key);
} else {
dom.style.setProperty(key, value, priority);
}
}
}
}
styles[key] = value;
/**
* @param {Element & ElementCSSInlineStyle} dom
* @param {string | null} value
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [prev_styles]
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
*/
export function set_style(dom, value, prev_styles, next_styles) {
// @ts-expect-error
var prev = dom.__style;
if (value == null) {
dom.style.removeProperty(key);
if (hydrating || prev !== value) {
var next_style_attr = to_style(value, next_styles);
if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
if (next_style_attr == null) {
dom.removeAttribute('style');
} else {
dom.style.setProperty(key, value, important ? 'important' : '');
dom.style.cssText = next_style_attr;
}
}
// @ts-expect-error
dom.__style = value;
} else if (next_styles) {
if (Array.isArray(next_styles)) {
update_styles(dom, prev_styles?.[0], next_styles[0]);
update_styles(dom, prev_styles?.[1], next_styles[1], 'important');
} else {
update_styles(dom, prev_styles, next_styles);
}
}
return next_styles;
}

@ -14,6 +14,7 @@ import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
/**
* @param {Element} element
@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js';
* @returns {void}
*/
function dispatch_event(element, type) {
without_reactive_context(() => {
element.dispatchEvent(new CustomEvent(type));
});
}
/**

@ -44,11 +44,11 @@ export function init_operations() {
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = '';
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error
element_prototype.__styles = null;
element_prototype.__style = undefined;
// @ts-expect-error
element_prototype.__e = undefined;

@ -1,30 +1,26 @@
import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available
export const request_idle_callback =
const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */
let current_queued_micro_tasks = [];
let micro_tasks = [];
/** @type {Array<() => void>} */
let current_queued_idle_tasks = [];
let idle_tasks = [];
function process_micro_tasks() {
is_micro_task_queued = false;
const tasks = current_queued_micro_tasks.slice();
current_queued_micro_tasks = [];
function run_micro_tasks() {
var tasks = micro_tasks;
micro_tasks = [];
run_all(tasks);
}
function process_idle_tasks() {
is_idle_task_queued = false;
const tasks = current_queued_idle_tasks.slice();
current_queued_idle_tasks = [];
function run_idle_tasks() {
var tasks = idle_tasks;
idle_tasks = [];
run_all(tasks);
}
@ -32,32 +28,33 @@ function process_idle_tasks() {
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_micro_tasks);
if (micro_tasks.length === 0) {
queueMicrotask(run_micro_tasks);
}
current_queued_micro_tasks.push(fn);
micro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
if (!is_idle_task_queued) {
is_idle_task_queued = true;
request_idle_callback(process_idle_tasks);
if (idle_tasks.length === 0) {
request_idle_callback(run_idle_tasks);
}
current_queued_idle_tasks.push(fn);
idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (is_micro_task_queued) {
process_micro_tasks();
if (micro_tasks.length > 0) {
run_micro_tasks();
}
if (is_idle_task_queued) {
process_idle_tasks();
if (idle_tasks.length > 0) {
run_idle_tasks();
}
}

@ -250,8 +250,6 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
let uid = 1;
/**
* Create (or hydrate) an unique UID for the component instance.
*/
@ -260,12 +258,16 @@ export function props_id() {
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s')
hydrate_node.textContent?.startsWith(`#`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}
return 'c' + uid++;
// @ts-expect-error This way we ensure the id is unique even across Svelte runtimes
(window.__svelte ??= {}).uid ??= 1;
// @ts-expect-error
return `c${window.__svelte.uid++}`;
}

@ -39,9 +39,11 @@ export {
set_checked,
set_selected,
set_default_checked,
set_default_value
set_default_value,
CLASS,
STYLE
} from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js';
@ -146,7 +148,7 @@ export {
get,
safe_get,
invalidate_inner_signals,
flush_sync,
flushSync as flush,
tick,
untrack,
exclude_from_object,

@ -116,7 +116,7 @@ function get_derived_parent_effect(derived) {
* @param {Derived} derived
* @returns {T}
*/
export function execute_derived(derived) {
function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;

@ -6,15 +6,12 @@ import {
update_effect,
get,
is_destroying_effect,
is_flushing_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
set_is_flushing_effect,
set_signal_status,
untrack,
skip_reaction,
untracking
} from '../runtime.js';
import {
@ -85,13 +82,12 @@ function push_effect(effect, parent_effect) {
* @returns {Effect}
*/
function create_effect(type, fn, sync, push = true) {
var is_root = (type & ROOT_EFFECT) !== 0;
var parent_effect = active_effect;
var parent = active_effect;
if (DEV) {
// Ensure the parent is never an inspect effect
while (parent_effect !== null && (parent_effect.f & INSPECT_EFFECT) !== 0) {
parent_effect = parent_effect.parent;
while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) {
parent = parent.parent;
}
}
@ -106,7 +102,7 @@ function create_effect(type, fn, sync, push = true) {
fn,
last: null,
next: null,
parent: is_root ? null : parent_effect,
parent,
prev: null,
teardown: null,
transitions: null,
@ -118,17 +114,12 @@ function create_effect(type, fn, sync, push = true) {
}
if (sync) {
var previously_flushing_effect = is_flushing_effect;
try {
set_is_flushing_effect(true);
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
} else if (fn !== null) {
schedule_effect(effect);
@ -144,9 +135,9 @@ function create_effect(type, fn, sync, push = true) {
effect.teardown === null &&
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
if (!inert && !is_root && push) {
if (parent_effect !== null) {
push_effect(effect, parent_effect);
if (!inert && push) {
if (parent !== null) {
push_effect(effect, parent);
}
// if we're in a derived, add the effect there too
@ -399,7 +390,14 @@ export function destroy_effect_children(signal, remove_dom = false) {
while (effect !== null) {
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
// this is now an independent root
effect.parent = null;
} else {
destroy_effect(effect, remove_dom);
}
effect = next;
}
}

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

Loading…
Cancel
Save