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: jobs:
Tests: Tests:
permissions: {}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
@ -41,6 +42,7 @@ jobs:
env: env:
CI: true CI: true
Lint: Lint:
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
@ -61,6 +63,7 @@ jobs:
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail 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); } 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: Benchmarks:
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:

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

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

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

@ -17,6 +17,7 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: 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: 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> </picture>
</a> </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? ## What is Svelte?

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

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

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

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

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

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

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

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

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

@ -2,7 +2,7 @@
title: What are runes? 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. > A letter or mark used as a mystical or magic symbol.

@ -2,15 +2,11 @@
title: $effect 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. 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=)):
> [!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=)):
```svelte ```svelte
<script> <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" /> <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 ```svelte
<script> <script>
@ -50,7 +54,7 @@ You can return a function from `$effect`, which will run immediately before the
}, milliseconds); }, milliseconds);
return () => { 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 // a) immediately before the effect re-runs
// b) when the component is destroyed // b) when the component is destroyed
clearInterval(interval); 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> <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 ### 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=)): 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 --> <!-- prettier-ignore -->
```svelte ```svelte
/// App.svelte /// file: App.svelte
<script> <script>
import FancyInput from './FancyInput.svelte'; 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. 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
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y_autofocus -->
<input bind:value={name} 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 --> <!-- for some reason puts the comment and html on same line -->
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte
<div contenteditable="true" bind:innerHTML={html} /> <div contenteditable="true" bind:innerHTML={html}></div>
``` ```
## Dimensions ## Dimensions
@ -307,7 +307,7 @@ To get a reference to a DOM node, use `bind:this`. The value will be `undefined`
}); });
</script> </script>
<canvas bind:this={canvas} /> <canvas bind:this={canvas}></canvas>
``` ```
Components also support `bind:this`, allowing you to interact with component instances programmatically. 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() { function toggle() {
toggleValue = !toggleValue; theme = theme === 'dark' ? 'light' : 'dark';
} }
</script> </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. 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`: 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 ```svelte
<script> <script>
let count = +++$state(+++0+++)+++; let count = +++$state(0)+++;
</script> </script>
``` ```
@ -25,14 +25,14 @@ Nothing else changes. `count` is still the number itself, and you read and write
> [!DETAILS] Why we did this > [!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. > `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: 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 ```svelte
<script> <script>
let count = +++$state(+++0+++)+++; let count = $state(0);
---$:--- +++const+++ double = +++$derived(+++count * 2+++)+++; ---$:--- +++const+++ double = +++$derived(count * 2)+++;
</script> </script>
``` ```
@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
```svelte ```svelte
<script> <script>
let count = +++$state(+++0+++)+++; let count = $state(0);
---$:---+++$effect(() =>+++ { ---$:---+++$effect(() =>+++ {
if (count > 5) { if (count > 5) {
alert('Count is too high!'); 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> </script>
``` ```
Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs.
> [!DETAILS] Why we did this > [!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. > `$:` 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 > - executing dependencies as needed and therefore being immune to ordering problems
> - being TypeScript-friendly > - 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: 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 ```svelte
<script> <script>
---export let optional = 'unset'; ---export let optional = 'unset';---
export let required;--- ---export let required;---
+++let { optional = 'unset', required } = $props();+++ +++let { optional = 'unset', required } = $props();+++
</script> </script>
``` ```
@ -103,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
```svelte ```svelte
<script> <script>
---let klass = ''; ---let klass = '';---
export { klass as class};--- ---export { klass as class};---
+++let { class: klass, ...rest } = $props();+++ +++let { class: klass, ...rest } = $props();+++
</script> </script>
<button class={klass} {...---$$restProps---+++rest+++}>click me</button> <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 ```svelte
<!--- file: Pump.svelte ---> <!--- file: Pump.svelte --->
<script> <script>
---import { createEventDispatcher } from 'svelte'; ---import { createEventDispatcher } from 'svelte';---
const dispatch = createEventDispatcher(); ---const dispatch = createEventDispatcher();---
---
+++let { inflate, deflate } = $props();+++ +++let { inflate, deflate } = $props();+++
let power = $state(5); let power = $state(5);
</script> </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: 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` - bump core dependencies in your `package.json`
- migrate to runes (`let` -> `$state` etc) - migrate to runes (`let` `$state` etc)
- migrate to event attributes for DOM elements (`on:click` -> `onclick`) - migrate to event attributes for DOM elements (`on:click` `onclick`)
- migrate slot creations to render tags (`<slot />` -> `{@render children()}`) - migrate slot creations to render tags (`<slot />` `{@render children()}`)
- migrate slot usages to snippets (`<div slot="x">...</div>` -> `{#snippet x()}<div>...</div>{/snippet}`) - migrate slot usages to snippets (`<div slot="x">...</div>` `{#snippet x()}<div>...</div>{/snippet}`)
- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`) - 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. 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 use markdown here.
- You can also use code blocks here. - You can also use code blocks here.
- Usage: - Usage:
```tsx ```svelte
<main name="Arethra"> <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` 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` can only bind to an Identifier or MemberExpression
``` ```
### bind_group_invalid_snippet_parameter
```
Cannot `bind:group` to a snippet parameter
```
### bind_invalid_expression ### bind_invalid_expression
``` ```

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

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

@ -957,6 +957,7 @@ export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {
export interface HTMLDialogAttributes extends HTMLAttributes<HTMLDialogElement> { export interface HTMLDialogAttributes extends HTMLAttributes<HTMLDialogElement> {
open?: boolean | undefined | null; open?: boolean | undefined | null;
closedby?: 'any' | 'closerequest' | 'none' | undefined | null;
} }
export interface HTMLEmbedAttributes extends HTMLAttributes<HTMLEmbedElement> { export interface HTMLEmbedAttributes extends HTMLAttributes<HTMLEmbedElement> {
@ -1075,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
checked?: boolean | undefined | null; checked?: boolean | undefined | null;
dirname?: string | undefined | null; dirname?: string | undefined | null;
disabled?: boolean | undefined | null; disabled?: boolean | undefined | null;
files?: FileList | undefined | null;
form?: string | undefined | null; form?: string | undefined | null;
formaction?: string | undefined | null; formaction?: string | undefined | null;
formenctype?: formenctype?:
@ -1086,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null; formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null;
formnovalidate?: boolean | undefined | null; formnovalidate?: boolean | undefined | null;
formtarget?: string | undefined | null; formtarget?: string | undefined | null;
group?: any | undefined | null;
height?: number | string | undefined | null; height?: number | string | undefined | null;
indeterminate?: boolean | undefined | null; indeterminate?: boolean | undefined | null;
list?: string | 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 ## 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` > 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` can only bind to an Identifier or MemberExpression
## bind_group_invalid_snippet_parameter
> Cannot `bind:group` to a snippet parameter
## bind_invalid_expression ## bind_invalid_expression
> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair > Can only bind to an Identifier or MemberExpression or a `{get, set}` pair

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

@ -1,11 +1,9 @@
/** @import { Comment, Program } from 'estree' */ /** @import { Comment, Program } from 'estree' */
/** @import { Node } from 'acorn' */
import * as acorn from 'acorn'; import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript'; import { tsPlugin } from '@sveltejs/acorn-typescript';
import { locator } from '../../state.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true })); const ParserWithTS = acorn.Parser.extend(tsPlugin());
/** /**
* @param {string} source * @param {string} source
@ -48,7 +46,6 @@ export function parse(source, typescript, is_script) {
} }
} }
if (typescript) amend(source, ast);
add_comments(ast); add_comments(ast);
return /** @type {Program} */ (ast); return /** @type {Program} */ (ast);
@ -71,7 +68,6 @@ export function parse_expression_at(source, typescript, index) {
locations: true locations: true
}); });
if (typescript) amend(source, ast);
add_comments(ast); add_comments(ast);
return 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 * @param {Parser} parser
*/ */
function read_identifier(parser) { function read_identifier(parser) {
@ -574,7 +574,7 @@ function read_identifier(parser) {
let identifier = ''; 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); e.css_expected_identifier(start);
} }

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

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

@ -133,7 +133,13 @@ const css_visitors = {
node.metadata.is_global = node.selectors.length >= 1 && is_global(node); 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]; const first = node.selectors[0];
node.metadata.is_global_like ||= node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') || (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'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0; const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1; const NODE_DEFINITELY_EXISTS = 1;
const FORWARD = 0;
const BACKWARD = 1;
const whitelist_attribute_selector = new Map([ const whitelist_attribute_selector = new Map([
['details', ['open']], ['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) * Snippets encountered already (avoids infinite loops)
* @type {Set<Compiler.AST.SnippetBlock>} * @type {Set<Compiler.AST.SnippetBlock>}
@ -72,7 +96,8 @@ export function prune(stylesheet, element) {
apply_selector( apply_selector(
selectors, selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element element,
BACKWARD
) )
) { ) {
node.metadata.used = true; node.metadata.used = true;
@ -159,16 +184,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean} * @returns {boolean}
*/ */
function apply_selector(relative_selectors, rule, element) { function apply_selector(relative_selectors, rule, element, direction) {
const parent_selectors = relative_selectors.slice(); const rest_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop(); const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched = const matched =
!!relative_selector && !!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) && relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
apply_combinator(relative_selector, parent_selectors, rule, element); apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) { if (matched) {
if (!is_outer_global(relative_selector)) { 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} 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.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @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} * @returns {boolean}
*/ */
function apply_combinator(relative_selector, parent_selectors, rule, node) { function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
if (!relative_selector.combinator) return true; const combinator =
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
const name = relative_selector.combinator.name; if (!combinator) return true;
switch (name) { switch (combinator.name) {
case ' ': case ' ':
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; let parent_matched = false;
const path = node.metadata.path; for (const parent of parents) {
let i = path.length; if (apply_selector(rest_selectors, rule, parent, direction)) {
while (i--) {
const parent = path[i];
if (parent.type === 'SnippetBlock') {
if (seen.has(parent)) {
parent_matched = true; 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 '+':
case '~': { case '~': {
const siblings = get_possible_element_siblings(node, name === '+'); const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false; let sibling_matched = false;
for (const possible_sibling of siblings.keys()) { for (const possible_sibling of siblings.keys()) {
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true; 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; sibling_matched = true;
} }
} }
return ( return (
sibling_matched || sibling_matched ||
(get_element_parent(node) === null && (direction === BACKWARD &&
parent_selectors.every((selector) => is_global(selector, rule))) 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.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean} * @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 // Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = []; const has_selectors = [];
const other_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. // 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. // In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) { 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, // 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, // 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) {} } // 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 // :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 // 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; let matched = false;
for (const complex_selector of complex_selectors) { for (const complex_selector of complex_selectors) {
const selectors = truncate(complex_selector); const [first, ...rest] = truncate(complex_selector);
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator; // if it was just a :global(...)
// In .x:has(> y), we want to search for y, ignoring the left-most combinator if (!first) {
// (else it would try to walk further up and fail because there are no selectors left) complex_selector.metadata.used = true;
if (selectors.length > 0) { matched = true;
selectors[0] = { continue;
...selectors[0],
combinator: null
};
} }
const descendants = if (include_self) {
left_most_combinator.name === '+' || left_most_combinator.name === '~' const selector_including_self = [
? (sibling_elements ??= get_following_sibling_elements(element, include_self)) first.combinator ? { ...first, combinator: null } : first,
: left_most_combinator.name === '>' ...rest
? child_elements ];
: descendant_elements; if (apply_selector(selector_including_self, rule, element, FORWARD)) {
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)
) {
complex_selector.metadata.used = true; 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 args = selector.args;
const complex_selector = args.children[0]; 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 // 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) { if (is_global) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (apply_selector(relative, rule, element)) { } else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { } 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) { for (const complex_selector of parent.prelude.children) {
if ( 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.children.every((s) => is_global(s, parent))
) { ) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true; 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} operator
* @param {any} expected_value * @param {any} expected_value
@ -822,6 +712,84 @@ function unquote(str) {
return 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 * @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} * @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 {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 {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen * @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, 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>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map(); const result = new Map();
const path = node.metadata.path; const path = node.metadata.path;
@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) { while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[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]; const node = fragment.nodes[j];
if (node.type === 'RegularElement') { if (node.type === 'RegularElement') {
@ -876,23 +845,30 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result; 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)) { } else if (is_block(node)) {
if (node.type === 'SlotElement') { if (node.type === 'SlotElement') {
result.set(node, NODE_PROBABLY_EXISTS); 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); add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) { if (adjacent_only && has_definite_elements(possible_last_child)) {
return result; return result;
} }
} else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { } else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS); result.set(node, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings, } else if (node.type === 'RenderTag') {
// so we want to continue until we find a definite sibling even with the adjacent-only combinator 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]; current = path[i];
if (!current) break; if (!current) break;
@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current); seen.add(current);
for (const site of current.metadata.sites) { 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); add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) { 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) { if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />` // `{#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 {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} * @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>} */ /** @type {Array<Compiler.AST.Fragment | undefined | null>} */
let fragments = []; let fragments = [];
@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement': case 'SlotElement':
fragments.push(node.fragment); fragments.push(node.fragment);
break; 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 */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */
const result = new Map(); const result = new Map();
let exhaustive = node.type !== 'SlotElement'; let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) { for (const fragment of fragments) {
if (fragment == null) { if (fragment == null) {
@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) {
continue; continue;
} }
const map = loop_child(fragment.nodes, adjacent_only); const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map); exhaustive &&= has_definite_elements(map);
add_to_map(map, result); 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 * @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue} * @returns {NodeExistsValue}
*/ */
function higher_existence(exist1, exist2) { function higher_existence(exist1, exist2) {
// @ts-expect-error TODO figure out if this is a bug if (exist2 === undefined) return exist1;
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2; return exist1 > exist2 ? exist1 : exist2;
} }
/** /**
* @param {Compiler.AST.SvelteNode[]} children * @param {Compiler.AST.SvelteNode[]} children
* @param {Direction} direction
* @param {boolean} adjacent_only * @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>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */
const result = new Map(); 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]; const child = children[i];
if (child.type === 'RegularElement') { if (child.type === 'RegularElement') {
@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) {
} }
} else if (child.type === 'SvelteElement') { } else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS); 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)) { } 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); add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) { if (adjacent_only && has_definite_elements(child_result)) {
break; break;
} }
} }
i = direction === FORWARD ? i + 1 : i - 1;
} }
return result; return result;

@ -258,8 +258,18 @@ export function analyze_module(ast, options) {
{ {
scope, scope,
scopes, scopes,
// @ts-expect-error TODO analysis: /** @type {ComponentAnalysis} */ (analysis),
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 visitors
); );
@ -554,7 +564,7 @@ export function analyze_component(root, source, options) {
binding.declaration_kind !== 'import' binding.declaration_kind !== 'import'
) { ) {
binding.kind = 'state'; binding.kind = 'state';
binding.mutated = binding.updated = true; binding.mutated = true;
} }
} }
} }
@ -608,9 +618,7 @@ export function analyze_component(root, source, options) {
expression: null, expression: null,
derived_state: [], derived_state: [],
function_depth: scope.function_depth, function_depth: scope.function_depth,
instance_scope: instance.scope, reactive_statement: null
reactive_statement: null,
reactive_statements: new Map()
}; };
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -672,9 +680,7 @@ export function analyze_component(root, source, options) {
parent_element: null, parent_element: null,
has_props_rune: false, has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
instance_scope: instance.scope,
reactive_statement: null, reactive_statement: null,
reactive_statements: analysis.reactive_statements,
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
derived_state: [], derived_state: [],
@ -755,66 +761,62 @@ export function analyze_component(root, source, options) {
if (!should_ignore_unused) { if (!should_ignore_unused) {
warn_unused(analysis.css.ast); warn_unused(analysis.css.ast);
} }
}
outer: for (const node of analysis.elements) { for (const node of analysis.elements) {
if (node.metadata.scoped) { if (node.metadata.scoped && is_custom_element_node(node)) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them mark_subtree_dynamic(node.metadata.path);
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server }
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {AST.Attribute | undefined} */ let has_class = false;
let class_attribute = undefined; let has_style = false;
let has_spread = false;
let has_class_directive = false;
let has_style_directive = false;
for (const attribute of node.attributes) { 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 // 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) { // We need an empty class to generate the set_class() or class="" correctly
if (is_text_attribute(class_attribute)) { if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`; node.attributes.push(
} else { create_attribute('class', -1, -1, [
/** @type {AST.Text} */ {
const css_text = {
type: 'Text', type: 'Text',
data: ` ${analysis.css.hash}`, data: '',
raw: ` ${analysis.css.hash}`, raw: '',
start: -1, start: -1,
end: -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( node.attributes.push(
create_attribute('class', -1, -1, [ create_attribute('style', -1, -1, [
{ {
type: 'Text', type: 'Text',
data: analysis.css.hash, data: '',
raw: analysis.css.hash, raw: '',
start: -1, start: -1,
end: -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 { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler'; import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState { export interface AnalysisState {
scope: Scope; scope: Scope;
@ -19,13 +18,11 @@ export interface AnalysisState {
component_slots: Set<string>; component_slots: Set<string>;
/** Information about the current expression/directive/block value */ /** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null; expression: ExpressionMetadata | null;
derived_state: string[]; derived_state: { name: string; private: boolean }[];
function_depth: number; function_depth: number;
// legacy stuff // legacy stuff
instance_scope: Scope;
reactive_statement: null | ReactiveStatement; reactive_statement: null | ReactiveStatement;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
} }
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context< export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

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

@ -5,7 +5,7 @@ import {
is_text_attribute, is_text_attribute,
object object
} from '../../../utils/ast.js'; } 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 e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js'; import { binding_properties } from '../../bindings.js';
@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return; return;
} }
validate_no_const_assignment(node, node.expression, context.state.scope, true); validate_assignment(node, node.expression, context.state);
const assignee = node.expression; const assignee = node.expression;
const left = object(assignee); const left = object(assignee);
@ -184,14 +184,6 @@ export function BindDirective(node, context) {
) { ) {
e.bind_invalid_value(node.expression); 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') { if (node.name === 'group') {
@ -199,6 +191,10 @@ export function BindDirective(node, context) {
throw new Error('Cannot find declaration for bind:group'); 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, // 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 // 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 // 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); e.bindable_invalid_location(node);
} }
// We need context in case the bound prop is stale
context.state.analysis.needs_context = true;
break; break;
case '$host': case '$host':
@ -218,14 +221,6 @@ export function CallExpression(node, context) {
break; 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 // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') { if (rune === '$inspect' || rune === '$derived') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); context.next({ ...context.state, function_depth: context.state.function_depth + 1 });

@ -7,7 +7,7 @@ import { get_rune } from '../../scope.js';
* @param {Context} context * @param {Context} context
*/ */
export function ClassBody(node, context) { export function ClassBody(node, context) {
/** @type {string[]} */ /** @type {{name: string, private: boolean}[]} */
const derived_state = []; const derived_state = [];
for (const definition of node.body) { for (const definition of node.body) {
@ -18,7 +18,10 @@ export function ClassBody(node, context) {
) { ) {
const rune = get_rune(definition.value, context.state.scope); const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') { 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 !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' && grand_parent?.type !== 'SnippetBlock' &&
grand_parent?.type !== 'SvelteBoundary' && grand_parent?.type !== 'SvelteBoundary' &&
grand_parent?.type !== 'KeyBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') || ((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot'))) !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); const binding = context.state.scope.get(local_name);
if (binding) binding.reassigned = binding.updated = true; if (binding) binding.reassigned = true;
} }
} else { } else {
validate_export(node, context.state.scope, local_name); 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 ( if (
node.body.type === 'ExpressionStatement' && node.body.type === 'ExpressionStatement' &&

@ -1,4 +1,4 @@
/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */ /** @import { TaggedTemplateExpression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { is_pure } from './shared/utils.js'; import { is_pure } from './shared/utils.js';
@ -12,12 +12,5 @@ export function TaggedTemplateExpression(node, context) {
context.state.expression.has_state = true; 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(); context.next();
} }

@ -30,7 +30,7 @@ const non_interactive_roles = non_abstract_roles
// 'generic' is meant to have no semantic meaning. // '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. // '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) && !['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
!role?.superClass.some((classes) => classes.includes('widget')) !role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
); );
}) })
.concat( .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 { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */ /** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -10,12 +10,12 @@ import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
/** /**
* @param {AssignmentExpression | UpdateExpression} node * @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument * @param {Pattern | Expression} argument
* @param {AnalysisState} state * @param {AnalysisState} state
*/ */
export function validate_assignment(node, argument, 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') { if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name); const binding = state.scope.get(argument.name);
@ -38,16 +38,22 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node); e.snippet_parameter_assignment(node);
} }
} }
if ( if (
argument.type === 'MemberExpression' && argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' && argument.object.type === 'ThisExpression' &&
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && (((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.type === 'Literal' &&
argument.property.value && argument.property.value &&
typeof argument.property.value === 'string' && 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'); e.constant_assignment(node, 'derived state');
} }

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

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

@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js';
export function AnimateDirective(node, context) { export function AnimateDirective(node, context) {
const expression = const expression =
node.expression === null node.expression === null
? b.literal(null) ? b.null
: b.thunk(/** @type {Expression} */ (context.visit(node.expression))); : b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this // 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 !== 'prop' &&
binding.kind !== 'bindable_prop' && binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' && binding.kind !== 'raw_state' &&
binding.kind !== 'store_sub' &&
context.state.analysis.runes && context.state.analysis.runes &&
should_proxy(right, context.state.scope) && should_proxy(right, context.state.scope) &&
is_non_coercive_operator(operator) is_non_coercive_operator(operator)

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

@ -16,7 +16,7 @@ export function BinaryExpression(node, context) {
'$.strict_equals', '$.strict_equals',
/** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)), /** @type {Expression} */ (context.visit(node.right)),
operator === '!==' && b.literal(false) operator === '!==' && b.false
); );
} }
@ -25,7 +25,7 @@ export function BinaryExpression(node, context) {
'$.equals', '$.equals',
/** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)), /** @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' && node.callee.property.type === 'Identifier' &&
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name node.callee.property.name
) ) &&
node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
) { ) {
return b.call( return b.call(
node.callee, node.callee,

@ -143,7 +143,8 @@ export function EachBlock(node, context) {
const child_state = { const child_state = {
...context.state, ...context.state,
transform: { ...context.state.transform } transform: { ...context.state.transform },
store_to_invalidate
}; };
/** The state used when generating the key function, if necessary */ /** 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_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template = const is_single_child_not_needing_template =
trimmed.length === 1 && 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 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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
@ -19,29 +19,28 @@ export function IfBlock(node, context) {
let alternate_id; let alternate_id;
if (node.alternate) { if (node.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
alternate_id = context.state.scope.generate('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[]} */ /** @type {Expression[]} */
const args = [ const args = [
context.state.node, node.elseif ? b.id('$$anchor') : context.state.node,
b.arrow( b.arrow(
[b.id('$$render')], [b.id('$$render')],
b.block([ b.block([
b.if( b.if(
/** @type {Expression} */ (context.visit(node.test)), /** @type {Expression} */ (context.visit(node.test)),
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
? b.stmt(
b.call(
b.id('$$render'),
b.id(alternate_id),
node.alternate ? b.literal(false) : undefined
)
)
: undefined
) )
]) ])
) )
@ -69,7 +68,7 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the // ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it // transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local' // 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))); 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 { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */ /** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -20,16 +20,17 @@ import { build_getter } from '../utils.js';
import { import {
get_attribute_name, get_attribute_name,
build_attribute_value, build_attribute_value,
build_class_directives, build_set_attributes,
build_style_directives, build_set_class,
build_set_attributes build_set_style
} from './shared/element.js'; } from './shared/element.js';
import { process_children } from './shared/fragment.js'; import { process_children } from './shared/fragment.js';
import { import {
build_render_statement, build_render_statement,
build_template_chunk, build_template_chunk,
build_update_assignment, build_update_assignment,
get_expression_id get_expression_id,
memoize_expression
} from './shared/utils.js'; } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js'; import { visit_event_attribute } from './shared/events.js';
@ -214,21 +215,17 @@ export function RegularElement(node, context) {
const node_id = context.state.node; const node_id = context.state.node;
// Then do attributes
let is_attributes_reactive = has_spread;
if (has_spread) { if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes')); const attributes_id = b.id(context.state.scope.generate('attributes'));
build_set_attributes( build_set_attributes(
attributes, attributes,
class_directives,
style_directives,
context, context,
node, node,
node_id, node_id,
attributes_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
); );
// If value binding exists, that one takes care of calling $.init_select // If value binding exists, that one takes care of calling $.init_select
@ -269,13 +266,24 @@ export function RegularElement(node, context) {
continue; continue;
} }
const name = get_attribute_name(node, attribute);
if ( if (
!is_custom_element && !is_custom_element &&
!cannot_be_set_statically(attribute.name) && !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); let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
const 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) { if (name !== 'class' || value) {
context.state.template.push( 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 (has_state ? context.state.update : context.state.init).push(b.stmt(update));
? 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;
} }
} }
// 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 ( if (
is_load_error_element(node.name) && is_load_error_element(node.name) &&
@ -363,15 +381,14 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag'); trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) { 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( child_state.init.push(
b.stmt( b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_chunk(trimmed, context.visit, child_state).value
)
)
); );
}
} else { } else {
/** @type {Expression} */ /** @type {Expression} */
let arg = context.state.node; 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 * 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. * 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. * Returns true if attribute is deemed reactive, false otherwise.
* @param {AST.RegularElement} element * @param {AST.RegularElement} element
* @param {Identifier} node_id * @param {Identifier} node_id
* @param {AST.Attribute} attribute * @param {string} name
* @param {Expression} value
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @returns {boolean}
*/ */
function build_element_attribute_update_assignment( function build_element_attribute_update(element, node_id, name, value, attributes) {
element, if (name === 'muted') {
node_id, // Special case for Firefox who needs it set as a property in order to work
attribute, return b.assignment('=', b.member(node_id, b.id('muted')), value);
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;
} }
// Special case for Firefox who needs it set as a property in order to work if (name === 'value') {
if (name === 'muted') { return b.call('$.set_value', node_id, value);
state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
return false;
} }
/** @type {Statement} */ if (name === 'checked') {
let update; return b.call('$.set_checked', node_id, value);
}
if (name === 'class') { if (name === 'selected') {
if (attribute.metadata.needs_clsx) { return b.call('$.set_selected', node_id, value);
value = b.call('$.clsx', value);
} }
update = b.stmt( if (
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 we would just set the defaultValue property, it would override the value property, // 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, // 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 // 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)) (element.name === 'textarea' && element.fragment.nodes.length > 0))
) { ) {
update = b.stmt(b.call('$.set_default_value', node_id, value)); return b.call('$.set_default_value', node_id, value);
} else if ( }
if (
// See defaultValue comment // See defaultValue comment
name === 'defaultChecked' && name === 'defaultChecked' &&
attributes.some( attributes.some(
(attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true (attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
) )
) { ) {
update = b.stmt(b.call('$.set_default_checked', node_id, value)); return 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 { if (is_dom_property(name)) {
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute'; return b.assignment('=', b.member(node_id, name), value);
update = b.stmt( }
b.call(
callee, return b.call(
name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute',
node_id, node_id,
b.literal(name), b.literal(name),
value, value,
is_ignored(element, 'hydration_attribute_changed') && b.true 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 {Identifier} node_id
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {ComponentContext} context * @param {ComponentContext} context
* @returns {boolean}
*/ */
function build_custom_element_attribute_update_assignment(node_id, attribute, context) { function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
const state = context.state; const { value, has_state } = build_attribute_value(attribute.value, context);
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 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, // this is different from other updates — it doesn't get grouped,
// because set_custom_element_data may not be idempotent // because set_custom_element_data may not be idempotent
state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression)))); const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call;
return true;
} else { context.state.init.push(b.stmt(update));
state.init.push(update);
return false;
}
} }
/** /**
@ -656,29 +668,33 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id * @param {Identifier} node_id
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {ComponentContext} context * @param {ComponentContext} context
* @returns {boolean}
*/ */
function build_element_special_value_attribute(element, node_id, attribute, context) { function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state; 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) => 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( const inner_assignment = b.assignment(
'=', '=',
b.member(node_id, 'value'), b.member(node_id, 'value'),
b.conditional( 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 b.literal(''), // render null/undefined values as empty string to support placeholder options
value 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( const update = b.stmt(
is_select_with_value is_select_with_value
? b.sequence([ ? b.sequence([
@ -708,9 +724,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
value, value,
update update
); );
return true;
} else { } else {
state.init.push(update); state.init.push(update);
return false;
} }
} }

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

@ -1,6 +1,7 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */ /** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
/** /**
@ -35,6 +36,9 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const external_statements = []; const external_statements = [];
/** @type {Statement[]} */
const internal_statements = [];
const snippets_visits = []; const snippets_visits = [];
// Capture the `failed` implicit snippet prop // Capture the `failed` implicit snippet prop
@ -53,7 +57,20 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const init = []; const init = [];
context.visit(child, { ...context.state, 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); external_statements.push(...init);
}
} else { } else {
nodes.push(child); nodes.push(child);
} }
@ -63,6 +80,10 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
if (dev && internal_statements.length) {
block.body.unshift(...internal_statements);
}
const boundary = b.stmt( const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) 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 { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js'; import { determine_namespace_for_children } from '../../utils.js';
import { import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
build_attribute_value, import { build_render_statement, get_expression_id } from './shared/utils.js';
build_class_directives,
build_set_attributes,
build_style_directives
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
/** /**
* @param {AST.SvelteElement} node * @param {AST.SvelteElement} node
@ -77,36 +72,29 @@ export function SvelteElement(node, context) {
// Let bindings first, they can be used on attributes // 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 context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
// Then do attributes if (
let is_attributes_reactive = false; attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
if (attributes.length === 0) { attributes[0].name.toLowerCase() === 'class' &&
if (context.state.analysis.css.hash) { is_text_attribute(attributes[0])
inner_context.state.init.push( ) {
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash))) build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
); } else if (attributes.length) {
}
} else {
const attributes_id = b.id(context.state.scope.generate('attributes')); 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, // 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. // therefore we need to do the "how to set an attribute" logic at runtime.
is_attributes_reactive = build_set_attributes( build_set_attributes(
attributes, attributes,
class_directives,
style_directives,
inner_context, inner_context,
node, node,
element_id, element_id,
attributes_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
); );
} }
// 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))); const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
if (dev) { if (dev) {

@ -116,8 +116,7 @@ export function VariableDeclaration(node, context) {
} }
const args = /** @type {CallExpression} */ (init).arguments; const args = /** @type {CallExpression} */ (init).arguments;
const value = const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
let options = let options =
args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined; 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 { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js'; import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js'; import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../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'; import { build_template_chunk, get_expression_id } from './utils.js';
/** /**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element * @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id * @param {Identifier} element_id
* @param {Identifier} attributes_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( export function build_set_attributes(
attributes, attributes,
class_directives,
style_directives,
context, context,
element, element,
element_id, element_id,
attributes_id, attributes_id
preserve_attribute_case,
is_custom_element,
state
) { ) {
let is_dynamic = false; let is_dynamic = false;
@ -68,102 +67,48 @@ export function build_set_attributes(
} }
} }
const call = b.call( if (class_directives.length) {
'$.set_attributes', values.push(
element_id, b.prop(
is_dynamic ? attributes_id : b.literal(null), 'init',
b.object(values), b.array([b.id('$.CLASS')]),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), build_class_directives_object(class_directives, context)
preserve_attribute_case, )
is_custom_element,
is_ignored(element, 'hydration_attribute_changed') && b.true
); );
if (is_dynamic) { is_dynamic ||=
context.state.init.push(b.let(attributes_id)); class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
return true;
} }
context.state.init.push(b.stmt(call)); if (style_directives.length) {
return false; values.push(
} b.prop(
'init',
/** b.array([b.id('$.STYLE')]),
* Serializes each style directive into something like `$.set_style(element, style_property, value)` build_style_directives_object(style_directives, context)
* 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 (has_state || is_attributes_reactive) { is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
state.update.push(update);
} else {
state.init.push(update);
}
} }
}
/** const call = b.call(
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)` '$.set_attributes',
* 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,
element_id, element_id,
context, is_dynamic ? attributes_id : b.null,
is_attributes_reactive b.object(values),
) { element.metadata.scoped &&
const state = context.state; context.state.analysis.css.hash !== '' &&
for (const directive of class_directives) { b.literal(context.state.analysis.css.hash),
const { has_state, has_call } = directive.metadata.expression; is_ignored(element, 'hydration_attribute_changed') && b.true
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));
if (is_attributes_reactive || has_state) { if (is_dynamic) {
state.update.push(update); context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
} else { } 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) { export function build_attribute_value(value, context, memoize = (value) => value) {
if (value === true) { 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) { if (!Array.isArray(value) || value.length === 1) {
@ -207,3 +152,120 @@ export function get_attribute_name(element, attribute) {
return attribute.name; 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 // When we hoist a function we assign an array with the function and all
// hoisted closure params. // hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params]; const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args); delegated_assignment = b.array(args);
} else { } else {
delegated_assignment = handler; delegated_assignment = handler;
} }
} else {
delegated_assignment = handler;
}
context.state.init.push( context.state.init.push(
b.stmt( b.stmt(
@ -123,13 +127,21 @@ export function build_event_handler(node, metadata, context) {
} }
// function declared in the script // function declared in the script
if ( if (handler.type === 'Identifier') {
handler.type === 'Identifier' && const binding = context.state.scope.get(handler.name);
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
) { if (binding?.is_function()) {
return handler; 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) { if (metadata.has_call) {
// memoize where necessary // memoize where necessary
const id = b.id(context.state.scope.generate('event_handler')); const id = b.id(context.state.scope.generate('event_handler'));

@ -144,6 +144,17 @@ function is_static_element(node, state) {
return false; 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') { if (node.name === 'option' && attribute.name === 'value') {
return false; return false;
} }

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

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

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */ /** @import { BlockStatement, Expression, IfStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.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 * @param {ComponentContext} context
*/ */
export function IfBlock(node, context) { export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); 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))); 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( 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 = const fallback =
node.fragment.nodes.length === 0 node.fragment.nodes.length === 0
? b.literal(null) ? b.null
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment))); : b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call( const slot = b.call(

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

@ -1,4 +1,4 @@
/** @import { Expression, Literal } from 'estree' */ /** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */ /** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { import {
@ -24,6 +24,7 @@ import {
is_content_editable_binding, is_content_editable_binding,
is_load_error_element is_load_error_element
} from '../../../../../../utils.js'; } from '../../../../../../utils.js';
import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
@ -47,9 +48,6 @@ export function build_element_attributes(node, context) {
let content = null; let content = null;
let has_spread = false; 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(); let events_to_capture = new Set();
for (const attribute of node.attributes) { 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 // the defaultValue/defaultChecked properties don't exist as attributes
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') { if (attribute.name === 'class') {
class_index = attributes.length;
if (attribute.metadata.needs_clsx) { if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({ attributes.push({
...attribute, ...attribute,
value: { value: {
.../** @type {AST.ExpressionTag} */ (attribute.value), .../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash expression: b.call(
? b.binary( '$.clsx',
'+', /** @type {AST.ExpressionTag} */ (attribute.value).expression
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
) )
: clsx_value
} }
}); });
} else { } else {
attributes.push(attribute); attributes.push(attribute);
} }
} else { } else {
if (attribute.name === 'style') {
style_index = attributes.length;
}
attributes.push(attribute); 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) { if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context); build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
} else { } else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.value === true || is_text_attribute(attribute)) {
const name = get_attribute_name(node, 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( build_attribute_value(
attribute.value, attribute.value,
context, context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
) )
).value; ).value;
if (name === 'class' && css_hash) {
literal_value = (String(literal_value) + ' ' + css_hash).trim();
}
if (name !== 'class' || literal_value) { if (name !== 'class' || literal_value) {
context.state.template.push( context.state.template.push(
b.literal( b.literal(
@ -264,21 +239,33 @@ export function build_element_attributes(node, context) {
) )
); );
} }
continue; continue;
} }
const name = get_attribute_name(node, attribute);
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) 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( context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
); );
} }
} }
}
if (events_to_capture.size !== 0) { if (events_to_capture.size !== 0) {
for (const event of events_to_capture) { for (const event of events_to_capture) {
@ -322,7 +309,7 @@ function build_element_spread_attributes(
let styles; let styles;
let flags = 0; let flags = 0;
if (class_directives.length > 0 || context.state.analysis.css.hash) { if (class_directives.length) {
const properties = class_directives.map((directive) => const properties = class_directives.map((directive) =>
b.init( b.init(
directive.name, directive.name,
@ -331,11 +318,6 @@ function build_element_spread_attributes(
: /** @type {Expression} */ (context.visit(directive.expression)) : /** @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); 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)); context.state.template.push(b.call('$.spread_attributes', ...args));
} }
/** /**
* *
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
* @param {AST.Attribute | null} class_attribute * @param {Expression} expression
* @returns * @param {ComponentContext} context
* @param {string | null} hash
*/ */
function build_class_directives(class_directives, class_attribute) { function build_attr_class(class_directives, expression, context, hash) {
const expressions = class_directives.map((directive) => /** @type {ObjectExpression | undefined} */
b.conditional(directive.expression, b.literal(directive.name), b.literal('')) 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); let css_hash;
const last = chunks.at(-1);
if (last?.type === 'Text') { if (hash) {
last.data += ' '; if (expression.type === 'Literal' && typeof expression.value === 'string') {
last.raw += ' '; expression.value = (expression.value + ' ' + hash).trim();
} else if (last) { } else {
chunks.push({ css_hash = b.literal(hash);
type: 'Text',
start: -1,
end: -1,
data: ' ',
raw: ' '
});
} }
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 b.call('$.attr_class', expression, css_hash, directives);
return class_attribute;
} }
/** /**
*
* @param {AST.StyleDirective[]} style_directives * @param {AST.StyleDirective[]} style_directives
* @param {AST.Attribute | null} style_attribute * @param {Expression} expression
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
function build_style_directives(style_directives, style_attribute, context) { function build_attr_style(style_directives, expression, context) {
const styles = style_directives.map((directive) => { /** @type {ArrayExpression | ObjectExpression | undefined} */
let value = let directives;
if (style_directives.length) {
let normal_properties = [];
let important_properties = [];
for (const directive of style_directives) {
const expression =
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true); : 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')) { 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 = if (important_properties.length) {
style_attribute === null directives = b.array([b.object(normal_properties), b.object(important_properties)]);
? b.object(styles) } else {
: b.call( directives = b.object(normal_properties);
'$.merge_styles', }
build_attribute_value(style_attribute.value, context, true), }
b.object(styles)
);
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 { Context, Visitor } from 'zimmerframe' */
/** @import { AST, Binding, DeclarationKind } from '#compiler' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js'; 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 { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
export 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 { export class Scope {
/** @type {ScopeRoot} */ /** @type {ScopeRoot} */
root; root;
@ -96,26 +180,15 @@ export class Scope {
} }
if (this.declarations.has(node.name)) { 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); e.declaration_duplicate(node, node.name);
} }
}
/** @type {Binding} */ const binding = new Binding(this, node, kind, declaration_kind, initial);
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
};
validate_identifier_name(binding, this.function_depth); 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); const binding = left && scope.get(left.name);
if (binding !== null && left !== binding.node) { if (binding !== null && left !== binding.node) {
binding.updated = true;
if (left === expression) { if (left === expression) {
binding.reassigned = true; binding.reassigned = true;
} else { } else {

@ -1,12 +1,5 @@
import type {
ClassDeclaration,
Expression,
FunctionDeclaration,
Identifier,
ImportDeclaration
} from 'estree';
import type { SourceMap } from 'magic-string'; 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 { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
@ -241,6 +234,20 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
hmr: CompileOptions['hmr']; 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 = export type DeclarationKind =
| 'var' | 'var'
| 'let' | 'let'
@ -251,66 +258,6 @@ export type DeclarationKind =
| 'rest_param' | 'rest_param'
| 'synthetic'; | '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 { export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */ /** All the bindings that are referenced inside this expression */
dependencies: Set<Binding>; dependencies: Set<Binding>;
@ -322,5 +269,7 @@ export interface ExpressionMetadata {
export * from './template.js'; export * from './template.js';
export { Binding, Scope } from '../phases/scope.js';
// TODO this chain is a bit weird // TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js'; export { ReactiveStatement } from '../phases/types.js';

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

@ -33,6 +33,7 @@ export const UNINITIALIZED = Symbol();
export const FILENAME = Symbol('filename'); export const FILENAME = Symbol('filename');
export const HMR = Symbol('hmr'); 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_SVG = 'http://www.w3.org/2000/svg';
export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */ /** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.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 { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js'; import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.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. * `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.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component; * Unlike `$effect`, the provided function only runs once.
* 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. * 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 * @template T
* @param {() => NotFunction<T> | Promise<NotFunction<T>> | (() => any)} fn * @param {() => NotFunction<T> | Promise<NotFunction<T>> | (() => any)} fn
@ -206,15 +207,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] }); return (l.u ??= { a: [], b: [], m: [] });
} }
/** export { flushSync } from './internal/client/runtime.js';
* 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 { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js'; export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js'; export { tick, untrack } from './internal/client/runtime.js';

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

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

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

@ -9,16 +9,16 @@ import {
set_hydrating set_hydrating
} from '../hydration.js'; } from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.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 {TemplateNode} node
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn * @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => 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 {[number,number]} [elseif]
* @returns {void} * @returns {void}
*/ */
export function if_block(node, fn, elseif = false) { export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating) { if (hydrating && root_index === 0) {
hydrate_next(); hydrate_next();
} }
@ -33,26 +33,44 @@ export function if_block(node, fn, elseif = false) {
/** @type {UNINITIALIZED | boolean | null} */ /** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED; var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0; var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0;
var has_branch = false; 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; has_branch = true;
update_branch(flag, fn); update_branch(flag, fn);
}; };
const update_branch = ( const update_branch = (
/** @type {boolean | null} */ new_condition, /** @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; 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 */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false; let mismatch = false;
if (hydrating) { if (hydrating && hydrate_index !== -1) {
const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; 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) { if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // 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_hydrate_node(anchor);
set_hydrating(false); set_hydrating(false);
mismatch = true; 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) { if (alternate_effect) {
resume_effect(alternate_effect); resume_effect(alternate_effect);
} else if (fn) { } else if (fn) {
alternate_effect = branch(() => fn(anchor)); alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index]));
} }
if (consequent_effect) { if (consequent_effect) {

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

@ -1,120 +1,47 @@
import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
/** /**
* @param {SVGElement} dom * @param {Element} dom
* @param {string} value * @param {boolean | number} is_html
* @param {string | null} value
* @param {string} [hash] * @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) { export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev_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.
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name; var prev = dom.__className;
} 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;
}
}
/** if (hydrating || prev !== value) {
* @param {MathMLElement} dom var next_class_name = to_class(value, hash, next_classes);
* @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 && dom.getAttribute('class') === next_class_name) { if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// In case of hydration don't reset the class as it's already correct. // Removing the attribute when the value is only an empty string causes
// @ts-expect-error need to add __className to patched prototype // performance issues vs simply making the className an empty string. So
dom.__className = next_class_name; // we should only remove the class if the the value is nullish
} else if ( // and there no hash/directives :
prev_class_name !== next_class_name || if (next_class_name == null) {
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class'); dom.removeAttribute('class');
} else if (is_html) {
dom.className = next_class_name;
} else { } else {
dom.setAttribute('class', next_class_name); 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 // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; dom.__className = value;
var next_class_name = to_class(value, hash); } 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) { if (prev_classes == null || is_present !== !!prev_classes[key]) {
// In case of hydration don't reset the class as it's already correct. dom.classList.toggle(key, is_present);
// @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;
} }
// @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 // @ts-expect-error
var delegated = current_target['__' + event_name]; 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)) { if (is_array(delegated)) {
var [fn, ...data] = delegated; var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]); fn.apply(current_target, [event, ...data]);
@ -305,13 +311,11 @@ export function apply(
error = e; error = e;
} }
if (typeof handler === 'function') { if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
handler.apply(element, args);
} else if (has_side_effects || handler != null || error) {
const filename = component?.[FILENAME]; const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`; const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
const event_name = args[0].type; const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`; const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`'; const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
@ -321,4 +325,5 @@ export function apply(
throw error; 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 {Element & ElementCSSInlineStyle} dom
* @param {string} key * @param {Record<string, any>} prev
* @param {string} value * @param {Record<string, any>} next
* @param {boolean} [important] * @param {string} [priority]
*/ */
export function set_style(dom, key, value, important) { function update_styles(dom, prev = {}, next, priority) {
// @ts-expect-error for (var key in next) {
var styles = (dom.__styles ??= {}); var value = next[key];
if (styles[key] === value) { if (prev[key] !== value) {
return; 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) { if (hydrating || prev !== value) {
dom.style.removeProperty(key); 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 { } 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 { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
/** /**
* @param {Element} element * @param {Element} element
@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js';
* @returns {void} * @returns {void}
*/ */
function dispatch_event(element, type) { function dispatch_event(element, type) {
without_reactive_context(() => {
element.dispatchEvent(new CustomEvent(type)); element.dispatchEvent(new CustomEvent(type));
});
} }
/** /**

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

@ -1,30 +1,26 @@
import { run_all } from '../../shared/utils.js'; import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available // Fallback for when requestIdleCallback is not available
export const request_idle_callback = const request_idle_callback =
typeof requestIdleCallback === 'undefined' typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1) ? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback; : requestIdleCallback;
let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */ /** @type {Array<() => void>} */
let current_queued_micro_tasks = []; let micro_tasks = [];
/** @type {Array<() => void>} */ /** @type {Array<() => void>} */
let current_queued_idle_tasks = []; let idle_tasks = [];
function process_micro_tasks() { function run_micro_tasks() {
is_micro_task_queued = false; var tasks = micro_tasks;
const tasks = current_queued_micro_tasks.slice(); micro_tasks = [];
current_queued_micro_tasks = [];
run_all(tasks); run_all(tasks);
} }
function process_idle_tasks() { function run_idle_tasks() {
is_idle_task_queued = false; var tasks = idle_tasks;
const tasks = current_queued_idle_tasks.slice(); idle_tasks = [];
current_queued_idle_tasks = [];
run_all(tasks); run_all(tasks);
} }
@ -32,32 +28,33 @@ function process_idle_tasks() {
* @param {() => void} fn * @param {() => void} fn
*/ */
export function queue_micro_task(fn) { export function queue_micro_task(fn) {
if (!is_micro_task_queued) { if (micro_tasks.length === 0) {
is_micro_task_queued = true; queueMicrotask(run_micro_tasks);
queueMicrotask(process_micro_tasks);
} }
current_queued_micro_tasks.push(fn);
micro_tasks.push(fn);
} }
/** /**
* @param {() => void} fn * @param {() => void} fn
*/ */
export function queue_idle_task(fn) { export function queue_idle_task(fn) {
if (!is_idle_task_queued) { if (idle_tasks.length === 0) {
is_idle_task_queued = true; request_idle_callback(run_idle_tasks);
request_idle_callback(process_idle_tasks);
} }
current_queued_idle_tasks.push(fn);
idle_tasks.push(fn);
} }
/** /**
* Synchronously run any queued tasks. * Synchronously run any queued tasks.
*/ */
export function flush_tasks() { export function flush_tasks() {
if (is_micro_task_queued) { if (micro_tasks.length > 0) {
process_micro_tasks(); 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)); anchor.before(/** @type {Node} */ (dom));
} }
let uid = 1;
/** /**
* Create (or hydrate) an unique UID for the component instance. * Create (or hydrate) an unique UID for the component instance.
*/ */
@ -260,12 +258,16 @@ export function props_id() {
hydrating && hydrating &&
hydrate_node && hydrate_node &&
hydrate_node.nodeType === 8 && hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s') hydrate_node.textContent?.startsWith(`#`)
) { ) {
const id = hydrate_node.textContent.substring(1); const id = hydrate_node.textContent.substring(1);
hydrate_next(); hydrate_next();
return id; 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_checked,
set_selected, set_selected,
set_default_checked, set_default_checked,
set_default_value set_default_value,
CLASS,
STYLE
} from './dom/elements/attributes.js'; } 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 { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js'; export { set_style } from './dom/elements/style.js';
@ -146,7 +148,7 @@ export {
get, get,
safe_get, safe_get,
invalidate_inner_signals, invalidate_inner_signals,
flush_sync, flushSync as flush,
tick, tick,
untrack, untrack,
exclude_from_object, exclude_from_object,

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

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

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

Loading…
Cancel
Save