async-svelte-set
Rich Harris 3 weeks ago
commit d7d7c314cf

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: rebase batches after process, not during

@ -4,7 +4,6 @@ root = true
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true

@ -0,0 +1,69 @@
name: Autofix Lint
on:
issue_comment:
types: [created]
workflow_dispatch:
permissions: {}
jobs:
autofix-lint:
permissions:
contents: write # to push the generated types commit
pull-requests: read # to resolve the PR head ref
# prevents this action from running on forks
if: |
github.repository == 'sveltejs/svelte' &&
(
github.event_name == 'workflow_dispatch' ||
(
github.event.issue.pull_request != null &&
github.event.comment.body == '/autofix' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
)
)
runs-on: ubuntu-latest
steps:
- name: Get PR ref
if: github.event_name != 'workflow_dispatch'
id: pr
uses: actions/github-script@v8
with:
script: |
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
if (pull.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Cannot autofix: this PR is from a forked repository. The autofix workflow can only push to branches within this repository.'
});
core.setFailed('PR is from a fork');
}
core.setOutput('ref', pull.head.ref);
- uses: actions/checkout@v6
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
- uses: pnpm/action-setup@v4.3.0
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm -F svelte build
- name: Run prettier
run: pnpm format
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git diff --staged --quiet || git commit -m "chore: autofix"
git push origin HEAD

@ -31,6 +31,8 @@ packages/svelte/tests/parser-modern/samples/*/_actual.json
packages/svelte/tests/parser-modern/samples/*/output.json
packages/svelte/types
packages/svelte/compiler/index.js
playgrounds/sandbox/dist/*
playgrounds/sandbox/output/*
playgrounds/sandbox/src/*
**/node_modules

@ -51,6 +51,17 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio
Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read.
In addition, if an expression contains an [`await`](await-expressions), Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this...
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
let total = $derived(await a + b);
```
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.)
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
## Overriding derived values

@ -41,9 +41,11 @@ You can use `$effect` anywhere, not just at the top level of a component, as lon
> [!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==)).
An effect can return a _teardown function_ which will run immediately before the effect re-runs:
<!-- codeblock:start {"title":"Effect teardown"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let milliseconds = $state(1000);
@ -68,6 +70,7 @@ An effect can return a _teardown function_ which will run immediately before the
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
```
<!-- codeblock:end -->
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.
@ -206,9 +209,11 @@ Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)):
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template:
<!-- codeblock:start {"title":"$effect.tracking()"} -->
```svelte
<!--- file: App.svelte --->
<script>
console.log('in component setup:', $effect.tracking()); // false
@ -219,14 +224,27 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
<p>in template: {$effect.tracking()}</p> <!-- true -->
```
<!-- codeblock:end -->
It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
## `$effect.pending`
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)):
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries:
<!-- codeblock:start {"title":"$effect.pending"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise((f) => setTimeout(f, 500)); // artificial delay
return a + b;
}
</script>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
@ -236,6 +254,7 @@ When using [`await`](await-expressions) in components, the `$effect.pending()` r
<p>pending promises: {$effect.pending()}</p>
{/if}
```
<!-- codeblock:end -->
## `$effect.root`
@ -285,9 +304,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this...
<!-- codeblock:start {"title":"Setting state in effects (don't do this!)"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -311,11 +332,21 @@ You might be tempted to do something convoluted with effects to link one value t
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
...use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible:
<!-- codeblock:start {"title":"Setting state with function bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -335,6 +366,14 @@ Instead, use `oninput` callbacks or — better still — [function bindings](bin
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).

@ -64,8 +64,9 @@ let { a, b, c, ...others } = $props();
## Updating props
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state ([demo](/playground/untitled#H4sIAAAAAAAAE6WQ0WrDMAxFf0WIQR0Wmu3VTQJln7HsIfVcZubIxlbGRvC_DzuBraN92qPula50tODZWB1RPi_IX16jLALWSOOUq6P3-_ihLWftNEZ9TVeOWBNHlNhGFYznfqCBzeRdYHh6M_YVzsFNsNs3pdpGd4eBcqPVDMrNxNDBXeSRtXioDgO1zU8ataeZ2RE4Utao924RFXQ9iHXwvoPHKpW1xY4g_Bg0cSVhKS0p560Za95612ZC02ONrD8ZJYdZp_rGQ37ff_mSP86Np2TWZaNNmdcH56P4P67K66_SXoK9pG-5dF5Z9QEAAA==)):
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state:
<!-- codeblock:start {"title":"Temporarily updating props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -91,11 +92,13 @@ References to a prop inside a component update when the prop itself updates —
clicks (child): {count}
</button>
```
<!-- codeblock:end -->
While you can temporarily _reassign_ props, you should not _mutate_ props unless they are [bindable]($bindable).
If the prop is a regular object, the mutation will have no effect ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2W1QmorQgJXk0RC3PkBwiExG9WQrC17U4Es_ztKUkQp9OjxzM7bjcjtSKjwyfKNp1aLORA4b13ADHszUED1HFE-3eyaBcy-Mw_O5eFAg8xa1wb6T9eWhVgCKiyD9sZJ3XAjZnTWCzzuzfAKvbcjbPJieR2jm_uGy-InweXqtd0baaliBG0nFgW3kBIUNWYo9CGoxE-UsgvIpw2_oc9-LmAPJBCPDJCggqvlVtvdH9puErEMlvVg9HsVtzuoaojzkKKAfRuALVDfk5ZZW0fmy05wXcFdwyktlUs-KIinljTXrRVnm7-kL9dYLVbUAQAA)):
If the prop is a regular object, the mutation will have no effect:
<!-- codeblock:start {"title":"Non-reactive props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -118,9 +121,11 @@ If the prop is a regular object, the mutation will have no effect ([demo](/playg
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0U7DMAxFf8VESBuiauG1WycheOEbKA9p67FA6kSNszJV-XeUZhMw2GN8r-1znUmQ7FGU4pn2UqsOes-SlSGRia3S6ET5Mgk-2OiJBZGdOh6szd0eNcdaIx3-V28NMRI7UYq1awdleVNTzaq3ZmB43CndwXYwPSzyYn4dWxermqJRI4Np3rFlqODasWRcTtAaT1zCHYSbVU3r4nsyrdPMKTUFKDYiE4yfLEoePIbsQpqfy3_nOVMuJIqg0wk1RFg7GOuWfwEbz2wIDLVatR_VtLyBagNTHFIUMCqtoZXeIfAOU1JoUJsR2IC3nWTMjt7GM4yKdyBhlAMpesvhydCC0y_i0ZagHByMh26WzUhXUUxKnpbcVnBfUwhznJnNlac7JkuIURL-2VVfwxflyrWcSQIAAA==)):
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it:
<!-- codeblock:start {"title":"Invalid mutation","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -147,8 +152,19 @@ If the prop is a reactive state proxy, however, then mutations _will_ have an ef
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2VkIbUVoYFraCIh7vwA4eC4G9Wta1vxpgJZ_nfkBEQp9OjxzOzTRGHlkUQlXpy9G0gq1idCL43ppDrAD84HUYheGwqieo2CP3y2Z0EU3-En79fhRIaz1slA_-nKWSbLQVRiE9SgPTetbVkfvRsYzztttugHd8RiXU6vr-jisbWb8idhN7O3bEQhmN5ZVDyMlIorcOddv_Eufq4AGmJEuG5PilEjQrnRcoV7JCTUuJlGWq7-YHYjs7NwVhmtDnVcrlA3iLmzLLGTAdaB-j736h68Oxv-JM1I0AFjoG1OzPfX023c1nhobUoT39QeKsRzS8owM8DFTG_pE6dcVl70AQAA))
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates:
<!-- codeblock:start {"title":"Non-reactive fallback props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
</script>
<Child />
```
```svelte
<!--- file: Child.svelte --->
@ -163,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched —
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune.

@ -5,9 +5,11 @@ tags: rune-inspect
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)):
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@ -18,14 +20,17 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@ -38,6 +43,7 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)

@ -89,9 +89,11 @@ You can freely use destructuring and rest patterns in each blocks.
{#each expression, index}...{/each}
```
In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)):
In case you just want to render something `n` times, you can omit the `as` part:
<!-- codeblock:start {"title":"Chess board"} -->
```svelte
<!--- file: App.svelte --->
<div class="chess-board">
{#each { length: 8 }, rank}
{#each { length: 8 }, file}
@ -99,7 +101,22 @@ In case you just want to render something `n` times, you can omit the `as` part
{/each}
{/each}
</div>
<style>
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;
.black {
background: black;
}
}
</style>
```
<!-- codeblock:end -->
## Else blocks

@ -57,9 +57,11 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks ([demo](/playground/untitled#H4sIAAAAAAAAE12P0QrCMAxFfyWrwhSEvc8p-h1OcG5RC10bmkyQ0n-3HQPBx3vCPUmCemiDrOpLULYbUdXqTKR2Sj6UA7_RCKbMbvJ9Jg33XpMcW9uKQYEAIzJ3T4QD3LSUDE-PnYA4YET4uOkGMc3W5B3xZrtvbVP9HDas2GqiZHqhMW6Tr9jGbG_oOCMImcUCwrIpFk1FqRyqpRpn0cmjHdAvnrIzuscyq_4nd3dPPD01ukE_NA6qFj9hvMYvGjJADw8BAAA=))...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('alice')}
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)):
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@ -109,14 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
@ -141,17 +149,65 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@ -169,12 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@ -187,6 +285,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<!-- result will be <button>click me</button> -->
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
@ -256,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) ([demo](/playground/untitled#H4sIAAAAAAAAE3WPwY7CMAxEf8UyB1hRgdhjl13Bga8gHFJipEqtGyUGFUX5dxJUtEB3b9bYM_MckHVLWOKut50TMuC5tpbEY4GnuiGP5T6gXG0-ykLSB8vW2oW_UCNZq7Snv_Rjx0Kc4kpc-6OrrfwoVlK3uQ4CaGMgwsl1LUwXy0f54J9-KV4vf20cNo7YkMu22aqAz4-oOLUI9YKluDPF4h_at-hX5PFyzA1tZ84N3fGpf8YfUU6GvDumLqDKmEqCjjCHUEX4hqDTWCU5PJ6Or38c4g1cPu9tnAEAAA==)):
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@ -267,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{a} + {b} = {a + b}
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer

@ -54,9 +54,11 @@ A `bind:value` directive on an `<input>` element binds the input's `value` prope
<p>{message}</p>
```
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)):
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number:
<!-- codeblock:start {"title":"Numeric bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -74,6 +76,7 @@ In the case of a numeric input (`type="number"` or `type="range"`), the value wi
<p>{a} + {b} = {a + b}</p>
```
<!-- codeblock:end -->
If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`.
@ -144,10 +147,11 @@ Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs
## `<input bind:group>`
Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)):
Inputs that work together can use `bind:group`:
<!-- codeblock:start {"title":"bind:group"} -->
```svelte
<!--- file: BurritoChooser.svelte --->
<!--- file: App.svelte --->
<script>
let tortilla = $state('Plain');
@ -155,6 +159,8 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
let fillings = $state([]);
</script>
<h1>Customize your burrito</h1>
<!-- grouped radio inputs are mutually exclusive -->
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
@ -165,7 +171,17 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
<p>Tortilla: {tortilla}</p>
<p>Fillings: {fillings.join(', ') || 'None'}</p>
<style>
label {
display: block;
}
</style>
```
<!-- codeblock:end -->
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.

@ -25,9 +25,11 @@ The experimental flag will be removed in Svelte 6.
## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -43,6 +45,7 @@ When an `await` expression depends on a particular piece of state, changes to th
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
@ -59,8 +62,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p>
<p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@ -68,13 +71,18 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
async function one() { return 1; }
async function two() { return 2; }
/** @param {number} x */
async function one(x) { return x; }
/** @param {number} y */
async function two(y) { return y; }
let x = $state(1);
let y = $state(2);
// ---cut---
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning

@ -2,7 +2,66 @@
title: Context
---
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`...
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling').
By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component:
<!-- codeblock:start {"title":"Context","selected":"context.ts"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Parent from './Parent.svelte';
import Child from './Child.svelte';
</script>
<Parent>
<Child />
</Parent>
```
```svelte
<!--- file: Parent.svelte --->
<script>
import { setUserContext } from './context';
let { children } = $props();
setUserContext({ name: 'world' });
</script>
{@render children()}
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getUserContext } from './context';
const user = getUserContext();
</script>
<h1>hello {user.name}, inside Child.svelte</h1>
```
```ts
/// file: context.ts
import { createContext } from 'svelte';
interface User {
name: string;
}
export const [getUserContext, setUserContext] = createContext<User>();
```
<!-- codeblock:end -->
> [!NOTE] `createContext` was added in version 5.40. If you are using an earlier version of Svelte, you must use `setContext` and `getContext` instead.
This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) as shown above.
## `setContext` and `getContext`
As an alternative to `createContext`, you can use `setContext` and `getContext` directly. The parent component sets context with `setContext(key, value)`...
```svelte
<!--- file: Parent.svelte --->
@ -26,32 +85,28 @@ Context allows components to access values owned by parent components without pa
<h1>{message}, inside Child.svelte</h1>
```
This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)):
```svelte
<Parent>
<Child />
</Parent>
```
The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value.
> [!NOTE] `createContext` is preferred since it provides better type safety and makes it unnecessary to use keys.
In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions.
## Using context with state
You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))...
You can store reactive state in context...
<!-- codeblock:start {"title":"Context with state"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { setContext } from 'svelte';
import { setCounter } from './context.ts';
import Child from './Child.svelte';
let counter = $state({
count: 0
});
setContext('counter', counter);
setCounter(counter);
</script>
<button onclick={() => counter.count += 1}>
@ -61,12 +116,39 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA
<Child />
<Child />
<Child />
<button onclick={() => counter.count = 0}>
reset
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getCounter } from './context.ts';
const counter = getCounter();
</script>
<p>{counter.count}</p>
```
```ts
/// file: context.ts
import { createContext } from 'svelte';
interface Counter {
count: number;
}
export const [getCounter, setCounter] = createContext<Counter>();
```
<!-- codeblock:end -->
...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this...
```svelte
<button onclick={() => counter = { count: 0 }}>
<button onclick={() => counter = { count: 0 } }>
reset
</button>
```
@ -81,21 +163,7 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA
Svelte will warn you if you get it wrong.
## Type-safe context
As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key:
```ts
/// file: context.ts
// @filename: ambient.d.ts
interface User {}
// @filename: index.ts
// ---cut---
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
```
## Component testing
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
@ -140,7 +208,7 @@ export const myGlobalState = $state({
In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
<!--- file: App.svelte ---->
<!--- file: App.svelte --->
<script>
import { myGlobalState } from './state.svelte.js';

@ -143,7 +143,7 @@ The CSS in a component's `<style>` is scoped to that component. If a parent comp
</style>
```
If this impossible (for example, the child component comes from a library) you can use `:global` to override styles:
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>

@ -396,7 +396,7 @@ Invalid selector
### declaration_duplicate_module_import
```
Cannot declare a variable with the same name as an import inside `<script module>`
Cannot declare a variable with the same name as an import from `<script module>`
```
### derived_invalid_export

@ -42,6 +42,12 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
```
### invariant_violation
```
An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "%message%"
```
### lifecycle_outside_component
```

@ -1,5 +1,99 @@
# svelte
## 5.55.1
### Patch Changes
- fix: correctly handle bindings on the server ([#18009](https://github.com/sveltejs/svelte/pull/18009))
- fix: prevent hydration error on async `{@html ...}` ([#17999](https://github.com/sveltejs/svelte/pull/17999))
- fix: cleanup `superTypeParameters` in `ClassDeclarations`/`ClassExpression` ([#18015](https://github.com/sveltejs/svelte/pull/18015))
- fix: improve duplicate module import error message ([#18016](https://github.com/sveltejs/svelte/pull/18016))
- fix: reschedule new effects in prior batches ([#18021](https://github.com/sveltejs/svelte/pull/18021))
## 5.55.0
### Minor Changes
- feat: export TweenOptions, SpringOptions, SpringUpdateOptions and Updater from svelte/motion ([#17967](https://github.com/sveltejs/svelte/pull/17967))
### Patch Changes
- fix: ensure HMR wrapper forwards correct start/end nodes to active effect ([#17985](https://github.com/sveltejs/svelte/pull/17985))
## 5.54.1
### Patch Changes
- fix: hydration comments during hmr ([#17975](https://github.com/sveltejs/svelte/pull/17975))
- fix: null out `effect.b` in `destroy_effect` ([#17980](https://github.com/sveltejs/svelte/pull/17980))
- fix: group sync statements ([#17977](https://github.com/sveltejs/svelte/pull/17977))
- fix: defer batch resolution until earlier intersecting batches have committed ([#17162](https://github.com/sveltejs/svelte/pull/17162))
- fix: properly invoke `iterator.return()` during reactivity loss check ([#17966](https://github.com/sveltejs/svelte/pull/17966))
- fix: remove trailing semicolon from {@const} tag printer ([#17962](https://github.com/sveltejs/svelte/pull/17962))
## 5.54.0
### Minor Changes
- feat: allow `css`, `runes`, `customElement` compiler options to be functions ([#17951](https://github.com/sveltejs/svelte/pull/17951))
### Patch Changes
- fix: reinstate reactivity loss tracking ([#17801](https://github.com/sveltejs/svelte/pull/17801))
## 5.53.13
### Patch Changes
- fix: ensure `$inspect` after top level await doesn't break builds ([#17943](https://github.com/sveltejs/svelte/pull/17943))
- fix: resume inert effects when they come from offscreen ([#17942](https://github.com/sveltejs/svelte/pull/17942))
- fix: don't eagerly access not-yet-initialized functions in template ([#17938](https://github.com/sveltejs/svelte/pull/17938))
- fix: discard batches made obsolete by commit ([#17934](https://github.com/sveltejs/svelte/pull/17934))
- fix: ensure "is standalone child" is correctly reset ([#17944](https://github.com/sveltejs/svelte/pull/17944))
- fix: remove nodes in boundary when work is pending and HMR is active ([#17932](https://github.com/sveltejs/svelte/pull/17932))
## 5.53.12
### Patch Changes
- fix: update `select.__value` on `change` ([#17745](https://github.com/sveltejs/svelte/pull/17745))
- chore: add `invariant` helper for debugging ([#17929](https://github.com/sveltejs/svelte/pull/17929))
- fix: ensure deriveds values are correct across batches ([#17917](https://github.com/sveltejs/svelte/pull/17917))
- fix: handle async RHS in `assignment_value_stale` ([#17925](https://github.com/sveltejs/svelte/pull/17925))
- fix: avoid traversing clean roots ([#17928](https://github.com/sveltejs/svelte/pull/17928))
## 5.53.11
### Patch Changes
- fix: remove `untrack` circular dependency ([#17910](https://github.com/sveltejs/svelte/pull/17910))
- fix: recover from errors that leave a corrupted effect tree ([#17888](https://github.com/sveltejs/svelte/pull/17888))
- fix: properly lazily evaluate RHS when checking for `assignment_value_stale` ([#17906](https://github.com/sveltejs/svelte/pull/17906))
- fix: resolve boundary in correct batch when hydrating ([#17914](https://github.com/sveltejs/svelte/pull/17914))
- chore: rebase batches after process, not during ([#17900](https://github.com/sveltejs/svelte/pull/17900))
## 5.53.10
### Patch Changes

@ -16,7 +16,7 @@
## declaration_duplicate_module_import
> Cannot declare a variable with the same name as an import inside `<script module>`
> Cannot declare a variable with the same name as an import from `<script module>`
## derived_invalid_export

@ -34,6 +34,10 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
## invariant_violation
> An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "%message%"
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.53.10",
"version": "5.55.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -138,14 +138,15 @@
"templating"
],
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"build": "rollup -c && pnpm generate",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate": "node scripts/process-messages && node ./scripts/generate-types.js",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"prepublishOnly": "pnpm build && node scripts/check-treeshakeability.js",
"knip": "pnpm dlx knip"
},
"devDependencies": {
@ -175,9 +176,9 @@
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"devalue": "^5.6.4",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -117,12 +117,12 @@ export function declaration_duplicate(node, name) {
}
/**
* Cannot declare a variable with the same name as an import inside `<script module>`
* Cannot declare a variable with the same name as an import from `<script module>`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_duplicate_module_import(node) {
e(node, 'declaration_duplicate_module_import', `Cannot declare a variable with the same name as an import inside \`<script module>\`\nhttps://svelte.dev/e/declaration_duplicate_module_import`);
e(node, 'declaration_duplicate_module_import', `Cannot declare a variable with the same name as an import from \`<script module>\`\nhttps://svelte.dev/e/declaration_duplicate_module_import`);
}
/**

@ -23,6 +23,7 @@ export { print } from './print/index.js';
export function compile(source, options) {
source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -33,7 +34,9 @@ export function compile(source, options) {
const combined_options = {
...validated,
...parsed_options,
customElementOptions
customElementOptions,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
};
if (parsed.metadata.ts) {

@ -146,6 +146,8 @@ export function migrate(source, { filename, use_ts } = {}) {
...parsed_options,
customElementOptions,
filename: filename ?? UNKNOWN_FILENAME,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : () => 'external',
runes: 'runes' in parsed_options ? () => parsed_options.runes : () => undefined,
experimental: {
async: true
}

@ -138,11 +138,13 @@ const visitors = {
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
MethodDefinition(node, context) {

@ -345,6 +345,8 @@ export function analyze_component(root, source, options) {
let synthetic_stores_legacy_check = [];
const runes_option = options.runes?.({ filename: options.filename });
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -359,7 +361,7 @@ export function analyze_component(root, source, options) {
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
if (
options.runes === false ||
runes_option === false ||
!is_rune(name) ||
(declaration !== null &&
// const state = $state(0) is valid
@ -395,7 +397,7 @@ export function analyze_component(root, source, options) {
e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
}
if (options.runes !== false) {
if (runes_option !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
e.global_reference_invalid(references[0].node, name);
} else if (declaration !== null && is_rune(name)) {
@ -447,7 +449,7 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename);
const runes =
options.runes ??
runes_option ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) {
@ -463,7 +465,10 @@ export function analyze_component(root, source, options) {
}
}
const is_custom_element = !!options.customElementOptions || options.customElement;
const custom_element_from_option = options.customElement({ filename: options.filename });
const css = options.css({ filename: options.filename });
const custom_element = options.customElementOptions ?? custom_element_from_option;
const is_custom_element = !!options.customElementOptions || custom_element_from_option;
const name = module.scope.generate(options.name ?? component_name);
@ -491,7 +496,7 @@ export function analyze_component(root, source, options) {
maybe_runes:
!runes &&
// if they explicitly disabled runes, use the legacy behavior
options.runes !== false &&
runes_option !== false &&
![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name)
) &&
@ -523,8 +528,8 @@ export function analyze_component(root, source, options) {
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: is_custom_element,
inject_styles: options.css === 'injected' || is_custom_element,
custom_element,
inject_styles: css === 'injected' || is_custom_element,
accessors:
is_custom_element ||
(runes ? false : !!options.accessors) ||
@ -680,7 +685,7 @@ export function analyze_component(root, source, options) {
w.options_deprecated_accessors(attribute);
}
if (attribute.name === 'customElement' && !options.customElement) {
if (attribute.name === 'customElement' && !custom_element_from_option) {
w.options_missing_custom_element(attribute);
}
@ -1069,6 +1074,9 @@ function calculate_blockers(instance, analysis) {
let awaited = false;
/** @type {Array<ESTree.Statement | ESTree.VariableDeclarator>} */
let sync_group = [];
// TODO this should probably be attached to the scope?
const promises = b.id('$$promises');
@ -1083,6 +1091,13 @@ function calculate_blockers(instance, analysis) {
binding.blocker = blocker;
}
function flush_sync_group() {
if (sync_group.length === 0) return;
analysis.instance_body.async.push({ nodes: sync_group, has_await: false });
sync_group = [];
}
/**
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
@ -1144,6 +1159,9 @@ function calculate_blockers(instance, analysis) {
trace_references(declarator, reads, writes, instance.scope);
// Needs to happen before blocker computation
if (has_await) flush_sync_group();
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
@ -1156,11 +1174,12 @@ function calculate_blockers(instance, analysis) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
if (has_await) {
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({ nodes: [declarator], has_await: true });
} else {
sync_group.push(declarator);
}
}
}
} else if (awaited) {
@ -1172,6 +1191,9 @@ function calculate_blockers(instance, analysis) {
trace_references(node, reads, writes, instance.scope);
// Needs to happen before blocker computation
if (has_await) flush_sync_group();
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
@ -1182,15 +1204,20 @@ function calculate_blockers(instance, analysis) {
if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
}
if (has_await) {
analysis.instance_body.async.push({ nodes: [node], has_await: true });
} else {
analysis.instance_body.async.push({ node, has_await });
sync_group.push(node);
}
} else {
analysis.instance_body.sync.push(node);
}
}
flush_sync_group();
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();

@ -161,7 +161,6 @@ export function ensure_no_module_import_conflict(node, state) {
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
// TODO fix the message here
e.declaration_duplicate_module_import(node.id);
}
}

@ -595,7 +595,7 @@ export function client_component(analysis, options) {
);
}
const ce = options.customElementOptions ?? options.customElement;
const ce = analysis.custom_element;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};

@ -5,7 +5,8 @@ import * as b from '#compiler/builders';
import {
build_assignment_value,
get_attribute_expression,
is_event_attribute
is_event_attribute,
is_expression_async
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js';
@ -36,14 +37,6 @@ function is_non_coercive_operator(operator) {
return ['=', '||=', '&&=', '??='].includes(operator);
}
/** @type {Record<string, string>} */
const callees = {
'=': '$.assign',
'&&=': '$.assign_and',
'||=': '$.assign_or',
'??=': '$.assign_nullish'
};
/**
* @param {AssignmentOperator} operator
* @param {Pattern} left
@ -179,7 +172,7 @@ function build_assignment(operator, left, right, context) {
// in cases like `(object.items ??= []).push(value)`, we may need to warn
// if the value gets proxified, since the proxy _isn't_ the thing that
// will be pushed to. we do this by transforming it to something like
// `$.assign_nullish(object, 'items', [])`
// `$.assign(object, 'items', '??=', () => [])`
let should_transform =
dev &&
path.at(-1) !== 'ExpressionStatement' &&
@ -225,22 +218,23 @@ function build_assignment(operator, left, right, context) {
}
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];
return /** @type {Expression} */ (
context.visit(
b.call(
callee,
/** @type {Expression} */ (left.object),
/** @type {Expression} */ (
left.computed
? left.property
: b.literal(/** @type {Identifier} */ (left.property).name)
),
right,
b.literal(locate_node(left))
)
)
const needs_lazy_getter = operator !== '=';
const needs_async = needs_lazy_getter && is_expression_async(right);
/** @type {Expression} */
let e = b.call(
needs_async ? '$.assign_async' : '$.assign',
/** @type {Expression} */ (left.object),
/** @type {Expression} */ (
left.computed ? left.property : b.literal(/** @type {Identifier} */ (left.property).name)
),
b.literal(operator),
needs_lazy_getter ? b.arrow([], right, needs_async) : right,
b.literal(locate_node(left))
);
if (needs_async) {
e = b.await(e);
}
return /** @type {Expression} */ (context.visit(e));
}
return null;

@ -63,6 +63,7 @@ export function Fragment(node, context) {
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
is_standalone,
init: [],
snippets: [],
consts: [],
@ -128,7 +129,7 @@ export function Fragment(node, context) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, {
...context,
state: { ...state, is_standalone }
state
});
} else {
/** @type {(is_text: boolean) => Expression} */

@ -101,7 +101,7 @@ export function process_children(nodes, initial, is_element, context) {
if (is_static_element(node)) {
skipped += 1;
} else if (
node.type === 'EachBlock' &&
(node.type === 'EachBlock' || node.type === 'HtmlTag') &&
nodes.length === 1 &&
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
@ -109,8 +109,6 @@ export function process_children(nodes, initial, is_element, context) {
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else if (node.type === 'HtmlTag' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(
false,

@ -82,12 +82,16 @@ export class Memoizer {
async_values() {
if (this.#async.length === 0) return;
return b.array(this.#async.map((memo) => b.thunk(memo.expression, true)));
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true)));
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#sync.map((memo) => b.arrow([], memo.expression)));
}
}

@ -65,7 +65,7 @@ export function render_stylesheet(source, analysis, options) {
merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (dev && options.css === 'injected' && css.code) {
if (dev && analysis.inject_styles && css.code) {
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
}

@ -300,7 +300,7 @@ export function server_component(analysis, options) {
const body = [...state.hoisted, ...module.body];
if (analysis.css.ast !== null && options.css === 'injected' && !options.customElement) {
if (analysis.css.ast !== null && analysis.inject_styles && !analysis.custom_element) {
const hash = b.literal(analysis.css.hash);
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);

@ -47,63 +47,24 @@ export function transform_body(instance_body, runner, transform) {
// Thunks for the await expressions
if (instance_body.async.length > 0) {
const thunks = instance_body.async.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
transform(b.var(s.node.id, s.node.init))
);
const statements =
visited.type === 'VariableDeclaration'
? visited.declarations.map((node) => {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
) {
// this is an intermediate declaration created in VariableDeclaration.js;
// subsequent statements depend on it
return b.var(node.id, node.init);
}
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
})
: [];
if (statements.length === 1) {
const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
return b.thunk(statement.expression, s.has_await);
}
return b.thunk(b.block(statements), s.has_await);
}
const thunks = instance_body.async.map((entry) => {
/** @type {ESTree.Statement[]} */
const entry_statements = [];
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
for (const node of entry.nodes) {
entry_statements.push(...transform_async_node(node, transform));
}
if (s.node.type === 'ExpressionStatement') {
// the expression may be a $inspect call, which will be transformed into an empty statement
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
transform(s.node.expression)
);
if (expression.type === 'EmptyStatement') {
return null;
}
if (entry_statements.length === 0) {
// Keep indices stable for async sequencing while avoiding array holes in run([...]).
return b.thunk(b.void0, false);
}
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
if (entry_statements.length === 1 && entry_statements[0].type === 'ExpressionStatement') {
return b.thunk(entry_statements[0].expression, entry.has_await);
}
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
return b.thunk(b.block(entry_statements), entry.has_await);
});
// TODO get the `$$promises` ID from scope
@ -112,3 +73,63 @@ export function transform_body(instance_body, runner, transform) {
return statements;
}
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator} node
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @returns {ESTree.Statement[]}
*/
function transform_async_node(node, transform) {
if (node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
transform(b.var(node.id, node.init))
);
return visited.type === 'VariableDeclaration'
? visited.declarations.map((node) => {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
) {
// This intermediate declaration is created in VariableDeclaration.js;
// subsequent statements may depend on it.
return b.var(node.id, node.init);
}
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
})
: [];
}
if (node.type === 'ClassDeclaration') {
return [
b.stmt(
b.assignment(
'=',
node.id,
/** @type {ESTree.ClassExpression} */ ({ ...node, type: 'ClassExpression' })
)
)
];
}
if (node.type === 'ExpressionStatement') {
// The expression may be a $inspect call, which will be transformed into an empty statement.
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
transform(node.expression)
);
if (expression.type === 'EmptyStatement') {
return [];
}
if (expression.type === 'AwaitExpression') {
return [b.stmt(expression)];
}
return [b.stmt(b.unary('void', expression))];
}
const statement = /** @type {ESTree.Statement | ESTree.EmptyStatement} */ (transform(node));
return statement.type === 'EmptyStatement' ? [] : [statement];
}

@ -131,7 +131,7 @@ export interface ComponentAnalysis extends Analysis {
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
async: Array<{ nodes: Array<Statement | VariableDeclarator>; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

@ -592,8 +592,13 @@ const svelte_visitors = (comments) => ({
},
ConstTag(node, context) {
context.write('{@');
context.visit(node.declaration);
context.write('{@const ');
const declarators = node.declaration.declarations;
for (let i = 0; i < declarators.length; i++) {
if (i > 0) context.write(', ');
context.visit(declarators[i]);
}
context.write('}');
},

@ -73,9 +73,11 @@ export interface CompileOptions extends ModuleCompileOptions {
/**
* If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
*
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @default false
*/
customElement?: boolean;
customElement?: boolean | ((options: { filename: string }) => boolean);
/**
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
*
@ -101,8 +103,10 @@ export interface CompileOptions extends ModuleCompileOptions {
* - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root.
* - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files.
* This is always `'injected'` when compiling with `customElement` mode.
*
* You can also pass a function that receives `{ filename }` and returns either `'injected'` or `'external'`.
*/
css?: 'injected' | 'external';
css?: 'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external');
/**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
* It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -142,7 +146,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead.
* @default undefined
*/
runes?: boolean | undefined;
runes?: boolean | undefined | ((options: { filename: string }) => boolean | undefined);
/**
* If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`.
*
@ -248,18 +252,22 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
Required<CompileOptions>,
| keyof ModuleCompileOptions
| 'name'
| 'customElement'
| 'compatibility'
| 'outputFilename'
| 'cssOutputFilename'
| 'sourcemap'
| 'css'
| 'runes'
> & {
name: CompileOptions['name'];
customElement: (options: { filename: string }) => boolean;
outputFilename: CompileOptions['outputFilename'];
cssOutputFilename: CompileOptions['cssOutputFilename'];
sourcemap: CompileOptions['sourcemap'];
compatibility: Required<Required<CompileOptions>['compatibility']>;
runes: CompileOptions['runes'];
css: (options: { filename: string }) => 'injected' | 'external';
runes: (options: { filename: string }) => boolean | undefined;
customElementOptions: AST.SvelteOptions['customElement'];
hmr: CompileOptions['hmr'];
};

@ -36,6 +36,13 @@ export function assignment_pattern(left, right) {
* @returns {ESTree.ArrowFunctionExpression}
*/
export function arrow(params, body, async = false) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (async && body.type === 'AwaitExpression') {
if (!has_await_expression(body.argument)) {
return arrow(params, body.argument);
}
}
return {
type: 'ArrowFunctionExpression',
params,
@ -462,13 +469,6 @@ export function thunk(expression, async = false) {
* @returns {ESTree.Expression}
*/
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
if (
expression.async === false &&
expression.body.type === 'CallExpression' &&

@ -51,24 +51,28 @@ const common_options = {
const component_options = {
accessors: deprecate(w.options_deprecated_accessors, boolean(false)),
css: validator('external', (input) => {
if (input === true || input === false) {
throw_error(
'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true'
);
}
if (input === 'none') {
throw_error(
'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.'
);
}
/** @type {Validator<'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external'), (options: { filename: string }) => 'injected' | 'external'>} */
css: parametric(
/** @type {(options: { filename: string }) => 'injected' | 'external'} */ (() => 'external'),
(input) => {
if (input === true || input === false) {
throw_error(
'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true'
);
}
if (input === 'none') {
throw_error(
'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.'
);
}
if (input !== 'external' && input !== 'injected') {
throw_error(`css should be either "external" (default, recommended) or "injected"`);
}
if (input !== 'external' && input !== 'injected') {
throw_error(`css should be either "external" (default, recommended) or "injected"`);
}
return input;
}),
return /** @type {'external' | 'injected'} */ (input);
}
),
cssHash: fun(({ css, filename, hash }) => {
return `svelte-${hash(filename === '(unknown)' ? css : filename ?? css)}`;
@ -77,7 +81,17 @@ const component_options = {
// TODO this is a sourcemap option, would be good to put under a sourcemap namespace
cssOutputFilename: string(undefined),
customElement: boolean(false),
/** @type {Validator<boolean | ((options: { filename: string }) => boolean), (options: { filename: string }) => boolean>} */
customElement: parametric(
/** @type {(options: { filename: string }) => boolean} */ (() => false),
(input, keypath) => {
if (typeof input !== 'boolean') {
throw_error(`${keypath} should be true or false`);
}
return /** @type {boolean} */ (input);
}
),
discloseVersion: boolean(true),
@ -107,7 +121,8 @@ const component_options = {
preserveWhitespace: boolean(false),
runes: boolean(undefined),
/** @type {Validator<boolean | undefined | (() => boolean | undefined), () => boolean | undefined>} */
runes: parametric(() => /** @type {boolean | undefined} */ (undefined)),
hmr: boolean(false),
@ -318,6 +333,28 @@ function fun(fallback) {
});
}
/**
* @template {(...args: any[]) => any} F
* @param {F} fallback
* @param {(value: unknown, keypath: string) => ReturnType<F>} [normalize]
* @returns {Validator}
*/
function parametric(fallback, normalize = (value) => /** @type {ReturnType<F>} */ (value)) {
return validator(fallback, (input, keypath) => {
if (typeof input === 'function') {
/** @type {(...args: Parameters<F>) => ReturnType<F>} */
const normalized = (...args) => normalize(input(...args), keypath);
return /** @type {F} */ (/** @type {unknown} */ (normalized));
}
/** @type {(...args: Parameters<F>) => ReturnType<F>} */
const normalized = (..._args) => normalize(input, keypath);
return /** @type {F} */ (/** @type {unknown} */ (normalized));
});
}
/** @param {string} msg */
function throw_error(msg) {
e.options_invalid_value(null, msg);

@ -21,12 +21,21 @@ function compare(a, b, property, location) {
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} operator
* @param {any} rhs
* @param {string} location
*/
export function assign(object, property, value, location) {
export function assign(object, property, operator, rhs, location) {
return compare(
(object[property] = value),
operator === '='
? (object[property] = rhs)
: operator === '&&='
? (object[property] &&= rhs())
: operator === '||='
? (object[property] ||= rhs())
: operator === '??='
? (object[property] ??= rhs())
: null,
untrack(() => object[property]),
property,
location
@ -36,42 +45,21 @@ export function assign(object, property, value, location) {
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} operator
* @param {any} rhs
* @param {string} location
*/
export function assign_and(object, property, value, location) {
export async function assign_async(object, property, operator, rhs, location) {
return compare(
(object[property] &&= value),
untrack(() => object[property]),
property,
location
);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_or(object, property, value, location) {
return compare(
(object[property] ||= value),
untrack(() => object[property]),
property,
location
);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_nullish(object, property, value, location) {
return compare(
(object[property] ??= value),
operator === '='
? (object[property] = await rhs)
: operator === '&&='
? (object[property] &&= await rhs())
: operator === '||='
? (object[property] ||= await rhs())
: operator === '??='
? (object[property] ??= await rhs())
: null,
untrack(() => object[property]),
property,
location

@ -5,7 +5,7 @@ import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
import { active_effect, get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
@ -43,16 +43,21 @@ export function hmr(fn) {
if (ran) set_should_intro(false);
// preserve getters/setters
Object.defineProperties(
instance,
Object.getOwnPropertyDescriptors(
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props)
)
);
var result =
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props);
// a component is not guaranteed to return something and we can't invoke getOwnPropertyDescriptors on undefined
if (result) {
Object.defineProperties(instance, Object.getOwnPropertyDescriptors(result));
}
if (ran) set_should_intro(true);
});
// Forward the nodes from the inner effect to the outer active effect which would
// get them if the HMR wrapper wasn't there. Do this inside the block not outside
// so that HMR updates to the component will also update the nodes on the active effect.
/** @type {Effect} */ (active_effect).nodes = effect.nodes;
}, EFFECT_TRANSPARENT);
ran = true;

@ -218,8 +218,6 @@ export class Boundary {
this.is_pending = true;
this.#pending_effect = branch(() => pending(this.#anchor));
var batch = /** @type {Batch} */ (current_batch);
queue_micro_task(() => {
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
@ -238,14 +236,12 @@ export class Boundary {
this.#pending_effect = null;
});
this.#resolve(batch);
this.#resolve(/** @type {Batch} */ (current_batch));
}
});
}
#render() {
var batch = /** @type {Batch} */ (current_batch);
try {
this.is_pending = this.has_pending_snippet();
this.#pending_count = 0;
@ -262,7 +258,7 @@ export class Boundary {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
this.#pending_effect = branch(() => pending(this.#anchor));
} else {
this.#resolve(batch);
this.#resolve(/** @type {Batch} */ (current_batch));
}
} catch (error) {
this.error(error);
@ -275,21 +271,9 @@ export class Boundary {
#resolve(batch) {
this.is_pending = false;
// any effects that were previously deferred should be rescheduled —
// after the next traversal (which will happen immediately, due to the
// same update that brought us here) the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
batch.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
batch.schedule(e);
}
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
// any effects that were previously deferred should be transferred
// to the batch, which will flush in the next microtask
batch.transfer_effects(this.#dirty_effects, this.#maybe_dirty_effects);
}
/**

@ -480,6 +480,14 @@ function reconcile(state, array, anchor, flags, get_key) {
}
}
if ((effect.f & INERT) !== 0) {
resume_effect(effect);
if (is_animated) {
effect.nodes?.a?.unfix();
(to_animate ??= new Set()).delete(effect);
}
}
if ((effect.f & EFFECT_OFFSCREEN) !== 0) {
effect.f ^= EFFECT_OFFSCREEN;
@ -508,14 +516,6 @@ function reconcile(state, array, anchor, flags, get_key) {
}
}
if ((effect.f & INERT) !== 0) {
resume_effect(effect);
if (is_animated) {
effect.nodes?.a?.unfix();
(to_animate ??= new Set()).delete(effect);
}
}
if (effect !== current) {
if (seen !== undefined && seen.has(effect)) {
if (matched.length < stashed.length) {

@ -106,6 +106,9 @@ export function bind_select_value(select, get, set = get) {
set(value);
// @ts-ignore
select.__value = value;
if (current_batch !== null) {
batches.add(current_batch);
}

@ -1,7 +1,7 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { assign, assign_async } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';

@ -19,10 +19,10 @@ import {
import { Batch, current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
reactivity_loss_tracker,
derived,
derived_safe_equal,
set_from_async_derived
set_reactivity_loss_tracker
} from './deriveds.js';
import { aborted } from './effects.js';
@ -131,7 +131,7 @@ export function capture() {
}
if (DEV) {
set_from_async_derived(null);
set_reactivity_loss_tracker(null);
set_dev_stack(previous_dev_stack);
}
};
@ -163,11 +163,11 @@ export async function save(promise) {
* @returns {Promise<() => T>}
*/
export async function track_reactivity_loss(promise) {
var previous_async_effect = current_async_effect;
var previous_async_effect = reactivity_loss_tracker;
var value = await promise;
return () => {
set_from_async_derived(previous_async_effect);
set_reactivity_loss_tracker(previous_async_effect);
return value;
};
}
@ -209,8 +209,8 @@ export async function* for_await_track_reactivity_loss(iterable) {
yield value;
}
} finally {
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
if (normal_completion && iterator.return !== undefined) {
// If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
if (!normal_completion && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
@ -224,7 +224,7 @@ export function unset_context(deactivate_batch = true) {
if (deactivate_batch) current_batch?.deactivate();
if (DEV) {
set_from_async_derived(null);
set_reactivity_loss_tracker(null);
set_dev_stack(null);
}
}
@ -307,15 +307,16 @@ export function wait(blockers) {
* @returns {(skip?: boolean) => void}
*/
export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
var effect = /** @type {Effect} */ (active_effect);
var boundary = /** @type {Boundary} */ (effect.b);
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1, batch);
batch.increment(blocking);
batch.increment(blocking, effect);
return (skip = false) => {
boundary.update_pending_count(-1, batch);
batch.decrement(blocking, skip);
batch.decrement(blocking, effect, skip);
};
}

@ -38,6 +38,8 @@ import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
/** @type {Set<Batch>} */
const batches = new Set();
@ -88,20 +90,20 @@ var source_stacks = DEV ? new Set() : null;
let uid = 1;
export class Batch {
// for debugging. TODO remove once async is stable
id = uid++;
/**
* The current values of any sources that are updated in this batch
* The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
* They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>}
* @type {Map<Value, [any, boolean]>}
*/
current = new Map();
/**
* The values of any sources that are updated in this batch _before_ those updates took place.
* The values of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.#current`
* @type {Map<Source, any>}
* @type {Map<Value, any>}
*/
previous = new Map();
@ -119,14 +121,16 @@ export class Batch {
#discard_callbacks = new Set();
/**
* The number of async effects that are currently in flight
* Async effects that are currently in flight
* @type {Map<Effect, number>}
*/
#pending = 0;
#pending = new Map();
/**
* The number of async effects that are currently in flight, _not_ inside a pending boundary
* Async effects that are currently in flight, _not_ inside a pending boundary
* @type {Map<Effect, number>}
*/
#blocking_pending = 0;
#blocking_pending = new Map();
/**
* A deferred that resolves when the batch is committed, used with `settled()`
@ -141,6 +145,12 @@ export class Batch {
*/
#roots = [];
/**
* Effects created while this batch was active.
* @type {Effect[]}
*/
#new_effects = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>}
@ -170,7 +180,31 @@ export class Batch {
#blockers = new Set();
#is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
return this.is_fork || this.#blocking_pending.size > 0;
}
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
var skipped = false;
var e = effect;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
skipped = true;
break;
}
e = e.parent;
}
if (!skipped) {
return true;
}
}
}
return false;
}
/**
@ -207,9 +241,25 @@ export class Batch {
#process() {
if (flush_count++ > 1000) {
batches.delete(this);
infinite_loop_guard();
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
this.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
}
}
const roots = this.#roots;
this.#roots = [];
@ -228,7 +278,12 @@ export class Batch {
var updates = (legacy_updates = []);
for (const root of roots) {
this.#traverse(root, effects, render_effects);
try {
this.#traverse(root, effects, render_effects);
} catch (e) {
reset_all(root);
throw e;
}
}
// any writes should take effect in a subsequent batch
@ -244,7 +299,7 @@ export class Batch {
collected_effects = null;
legacy_updates = null;
if (this.#is_deferred() || this.#blockers.size > 0) {
if (this.#is_deferred() || this.#is_blocked()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
@ -252,7 +307,7 @@ export class Batch {
reset_branch(e, t);
}
} else {
if (this.#pending === 0) {
if (this.#pending.size === 0) {
batches.delete(this);
}
@ -363,17 +418,18 @@ export class Batch {
/**
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Source} source
* @param {any} value
* @param {Value} source
* @param {any} old_value
* @param {boolean} [is_derived]
*/
capture(source, value) {
if (value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, value);
capture(source, old_value, is_derived = false) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value);
}
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v);
this.current.set(source, [source.v, is_derived]);
batch_values?.set(source, source.v);
}
}
@ -387,19 +443,6 @@ export class Batch {
batch_values = null;
}
#revive() {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
this.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
}
}
flush() {
var source_stacks = DEV ? new Set() : null;
@ -407,12 +450,6 @@ export class Batch {
is_processing = true;
current_batch = this;
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
this.#revive();
}
this.#process();
} finally {
flush_count = 0;
@ -437,6 +474,15 @@ export class Batch {
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
batches.delete(this);
}
/**
* @param {Effect} effect
*/
register_created_effect(effect) {
this.#new_effects.push(effect);
}
#commit() {
@ -450,11 +496,13 @@ export class Batch {
/** @type {Source[]} */
var sources = [];
for (const [source, value] of this.current) {
for (const [source, [value, is_derived]] of this.current) {
if (batch.current.has(source)) {
if (is_earlier && value !== batch.current.get(source)) {
var batch_value = /** @type {[any, boolean]} */ (batch.current.get(source))[0]; // faster than destructuring
if (is_earlier && value !== batch_value) {
// bring the value up to date
batch.current.set(source, value);
batch.current.set(source, [value, is_derived]);
} else {
// same value or later batch has more recent value,
// no need to re-run these effects
@ -465,13 +513,19 @@ export class Batch {
sources.push(source);
}
if (sources.length === 0) {
continue;
}
// Re-run async/block effects that depend on distinct values changed in both batches
var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
if (others.length === 0) {
if (is_earlier) {
// this batch is now obsolete and can be discarded
batch.discard();
}
} else if (sources.length > 0) {
if (DEV) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
batch.activate();
/** @type {Set<Value>} */
@ -484,6 +538,26 @@ export class Batch {
mark_effects(source, others, marked, checked);
}
checked = new Map();
var current_unequal = [...batch.current.keys()].filter((c) =>
this.current.has(c) ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c : true
);
for (const effect of this.#new_effects) {
if (
(effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
depends_on(effect, current_unequal, checked)
) {
if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
set_signal_status(effect, DIRTY);
batch.schedule(effect);
} else {
batch.#dirty_effects.add(effect);
}
}
}
// Only apply and traverse when we know we triggered async work with marking the effects
if (batch.#roots.length > 0) {
batch.apply();
@ -491,7 +565,7 @@ export class Batch {
batch.#traverse(root, [], []);
}
batch.#roots.length = 0;
batch.#roots = [];
}
batch.deactivate();
@ -504,7 +578,6 @@ export class Batch {
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#revive();
batch.#process();
}
}
@ -512,21 +585,42 @@ export class Batch {
}
/**
*
* @param {boolean} blocking
* @param {Effect} effect
*/
increment(blocking) {
this.#pending += 1;
if (blocking) this.#blocking_pending += 1;
increment(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0;
this.#pending.set(effect, pending_count + 1);
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
this.#blocking_pending.set(effect, blocking_pending_count + 1);
}
}
/**
* @param {boolean} blocking
* @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
decrement(blocking, skip) {
this.#pending -= 1;
if (blocking) this.#blocking_pending -= 1;
decrement(blocking, effect, skip) {
let pending_count = this.#pending.get(effect) ?? 0;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
if (blocking_pending_count === 1) {
this.#blocking_pending.delete(effect);
} else {
this.#blocking_pending.set(effect, blocking_pending_count - 1);
}
}
if (this.#decrement_queued || skip) return;
this.#decrement_queued = true;
@ -537,6 +631,23 @@ export class Batch {
});
}
/**
* @param {Set<Effect>} dirty_effects
* @param {Set<Effect>} maybe_dirty_effects
*/
transfer_effects(dirty_effects, maybe_dirty_effects) {
for (const e of dirty_effects) {
this.#dirty_effects.add(e);
}
for (const e of maybe_dirty_effects) {
this.#maybe_dirty_effects.add(e);
}
dirty_effects.clear();
maybe_dirty_effects.clear();
}
/** @param {(batch: Batch) => void} fn */
oncommit(fn) {
this.#commit_callbacks.add(fn);
@ -582,17 +693,25 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
batch_values = new Map(this.current);
batch_values = new Map();
for (const [source, [value]] of this.current) {
batch_values.set(source, value);
}
// ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) {
if (batch === this) continue;
if (batch === this || batch.is_fork) continue;
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
var intersects = false;
var differs = false;
if (batch.id < this.id) {
for (const source of batch.current.keys()) {
for (const [source, [, is_derived]] of batch.current) {
// Derived values don't partake in the blocking mechanism, because a derived could
// be triggered in one batch already but not the other one yet, causing a false-positive
if (is_derived) continue;
intersects ||= this.current.has(source);
differs ||= !this.current.has(source);
}
@ -992,6 +1111,20 @@ function reset_branch(effect, tracked) {
}
}
/**
* Mark an entire effect tree clean following an error
* @param {Effect} effect
*/
function reset_all(effect) {
set_signal_status(effect, CLEAN);
var e = effect.first;
while (e !== null) {
reset_all(e);
e = e.next;
}
}
/**
* Creates a 'fork', in which state changes are evaluated but not applied to the DOM.
* This is useful for speculatively loading data (for example) when you suspect that
@ -1034,13 +1167,6 @@ export function fork(fn) {
source.v = value;
}
// make writable deriveds dirty, so they recalculate correctly
for (source of batch.current.keys()) {
if ((source.f & DERIVED) !== 0) {
set_signal_status(source, DIRTY);
}
}
return {
commit: async () => {
if (committed) {
@ -1057,7 +1183,7 @@ export function fork(fn) {
batch.is_fork = false;
// apply changes and update write versions so deriveds see the change
for (var [source, value] of batch.current) {
for (var [source, [value]] of batch.current) {
source.v = value;
source.wv = increment_write_version();
}
@ -1091,7 +1217,6 @@ export function fork(fn) {
}
if (!committed && batches.has(batch)) {
batches.delete(batch);
batch.discard();
}
}

@ -45,12 +45,16 @@ import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */
export let current_async_effect = null;
/**
* This allows us to track 'reactivity loss' that occurs when signals
* are read after a non-context-restoring `await`. Dev-only
* @type {{ effect: Effect, warned: boolean } | null}
*/
export let reactivity_loss_tracker = null;
/** @param {Effect | null} v */
export function set_from_async_derived(v) {
current_async_effect = v;
/** @param {{ effect: Effect, warned: boolean } | null} v */
export function set_reactivity_loss_tracker(v) {
reactivity_loss_tracker = v;
}
export const recent_async_deriveds = new Set();
@ -124,7 +128,12 @@ export function async_derived(fn, label, location) {
var deferreds = new Map();
async_effect(() => {
if (DEV) current_async_effect = active_effect;
if (DEV) {
reactivity_loss_tracker = {
effect: /** @type {Effect} */ (active_effect),
warned: false
};
}
var effect = /** @type {Effect} */ (active_effect);
@ -142,7 +151,9 @@ export function async_derived(fn, label, location) {
unset_context();
}
if (DEV) current_async_effect = null;
if (DEV) {
reactivity_loss_tracker = null;
}
var batch = /** @type {Batch} */ (current_batch);
@ -174,7 +185,9 @@ export function async_derived(fn, label, location) {
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
if (DEV) current_async_effect = null;
if (DEV) {
reactivity_loss_tracker = null;
}
if (decrement_pending) {
// don't trigger an update if we're only here because
@ -371,6 +384,7 @@ export function execute_derived(derived) {
* @returns {void}
*/
export function update_derived(derived) {
var old_value = derived.v;
var value = execute_derived(derived);
if (!derived.equals(value)) {
@ -382,6 +396,7 @@ export function update_derived(derived) {
// change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork || derived.deps === null) {
derived.v = value;
current_batch?.capture(derived, old_value, true);
// deriveds without dependencies should never be recomputed
if (derived.deps === null) {

@ -42,7 +42,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects } from './batch.js';
import { Batch, collected_effects, current_batch } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -120,6 +120,8 @@ function create_effect(type, fn) {
effect.component_function = dev_current_component_function;
}
current_batch?.register_created_effect(effect);
/** @type {Effect | null} */
var e = effect;
@ -559,6 +561,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes =
effect.ac =
effect.b =
null;
}

@ -231,7 +231,11 @@ export function internal_set(source, value, updated_during_traversal = null) {
execute_derived(derived);
}
update_derived_status(derived);
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values === null) {
update_derived_status(derived);
}
}
source.wv = increment_write_version();

@ -27,7 +27,7 @@ import {
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
destroy_derived_effects,
reactivity_loss_tracker,
execute_derived,
freeze_derived_effects,
recent_async_deriveds,
@ -58,6 +58,7 @@ import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js';
import * as w from './warnings.js';
let is_updating_effect = false;
@ -568,19 +569,20 @@ export function get(signal) {
}
if (DEV) {
// TODO reinstate this, but make it actually work
// if (current_async_effect) {
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
// var was_read = current_async_effect.deps?.includes(signal);
if (
!untracking &&
reactivity_loss_tracker &&
!reactivity_loss_tracker.warned &&
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0
) {
reactivity_loss_tracker.warned = true;
// if (!tracking && !untracking && !was_read) {
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
w.await_reactivity_loss(/** @type {string} */ (signal.label));
// var trace = get_error('traced at');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
// }
var trace = get_error('traced at');
// eslint-disable-next-line no-console
if (trace) console.warn(trace);
}
recent_async_deriveds.delete(signal);
@ -595,7 +597,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
var trace = get_error('traced at');
trace = get_error('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);

@ -468,10 +468,14 @@ export class Renderer {
}
this.local = other.local;
this.#out = other.#out.map((item) => {
if (item instanceof Renderer) {
item.subsume(item);
this.#out = other.#out.map((item, i) => {
const current = this.#out[i];
if (current instanceof Renderer && item instanceof Renderer) {
current.subsume(item);
return current;
}
return item;
});
this.promise = other.promise;

@ -1,4 +1,6 @@
import { DEV } from 'esm-env';
import { define_property } from './utils.js';
import * as e from './errors.js';
/**
* @param {string} label
@ -63,3 +65,15 @@ export function get_stack() {
return new_lines;
}
/**
* @param {boolean} condition
* @param {string} message
*/
export function invariant(condition, message) {
if (!DEV) {
throw new Error('invariant(...) was not guarded by if (DEV)');
}
if (!condition) e.invariant_violation(message);
}

@ -51,6 +51,23 @@ export function invalid_snippet_arguments() {
}
}
/**
* An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app please open an issue at https://github.com/sveltejs/svelte, citing the following message: "%message%"
* @param {string} message
* @returns {never}
*/
export function invariant_violation(message) {
if (DEV) {
const error = new Error(`invariant_violation\nAn invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "${message}"\nhttps://svelte.dev/e/invariant_violation`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invariant_violation`);
}
}
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name

@ -8,37 +8,3 @@ export interface TickContext {
};
settled: boolean;
}
export interface SpringOpts {
stiffness?: number;
damping?: number;
precision?: number;
}
export interface SpringUpdateOpts {
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
hard?: any;
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
soft?: string | number | boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
instant?: boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
preserveMomentum?: number;
}
export type Updater<T> = (target_value: T, value: T) => T;
export interface TweenedOptions<T> {
delay?: number;
duration?: number | ((from: T, to: T) => number);
easing?: (t: number) => number;
interpolate?: (a: T, b: T) => (t: number) => T;
}

@ -1,16 +1,49 @@
import { Readable, type Unsubscriber } from '../store/public.js';
import { SpringUpdateOpts, TweenedOptions, Updater, SpringOpts } from './private.js';
export interface SpringOptions {
stiffness?: number;
damping?: number;
precision?: number;
}
export interface SpringUpdateOptions {
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
hard?: any;
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
soft?: string | number | boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
instant?: boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
preserveMomentum?: number;
}
export type Updater<T> = (target_value: T, value: T) => T;
export interface TweenOptions<T> {
delay?: number;
duration?: number | ((from: T, to: T) => number);
easing?: (t: number) => number;
interpolate?: (a: T, b: T) => (t: number) => T;
}
// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
// this means both the Spring class and the Spring interface are merged into one with some things only
// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
export interface Spring<T> extends Readable<T> {
set(new_value: T, opts?: SpringUpdateOpts): Promise<void>;
set(new_value: T, opts?: SpringUpdateOptions): Promise<void>;
/**
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
*/
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
update: (fn: Updater<T>, opts?: SpringUpdateOptions) => Promise<void>;
/**
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
*/
@ -37,7 +70,7 @@ export interface Spring<T> extends Readable<T> {
* @since 5.8.0
*/
export class Spring<T> {
constructor(value: T, options?: SpringOpts);
constructor(value: T, options?: SpringOptions);
/**
* Create a spring whose value is bound to the return value of `fn`. This must be called
@ -53,7 +86,7 @@ export class Spring<T> {
* </script>
* ```
*/
static of<U>(fn: () => U, options?: SpringOpts): Spring<U>;
static of<U>(fn: () => U, options?: SpringOptions): Spring<U>;
/**
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
@ -63,7 +96,7 @@ export class Spring<T> {
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
*/
set(value: T, options?: SpringUpdateOpts): Promise<void>;
set(value: T, options?: SpringUpdateOptions): Promise<void>;
damping: number;
precision: number;
@ -81,8 +114,8 @@ export class Spring<T> {
}
export interface Tweened<T> extends Readable<T> {
set(value: T, opts?: TweenedOptions<T>): Promise<void>;
update(updater: Updater<T>, opts?: TweenedOptions<T>): Promise<void>;
set(value: T, opts?: TweenOptions<T>): Promise<void>;
update(updater: Updater<T>, opts?: TweenOptions<T>): Promise<void>;
}
export { prefersReducedMotion, spring, tweened, Tween } from './index.js';

@ -1,6 +1,6 @@
/** @import { Task } from '#client' */
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
/** @import { Spring as SpringStore } from './public.js' */
/** @import { TickContext } from './private.js' */
/** @import { Spring as SpringStore, SpringOptions, SpringUpdateOptions } from './public.js' */
import { writable } from '../store/shared/index.js';
import { loop } from '../internal/client/loop.js';
import { raf } from '../internal/client/timing.js';
@ -62,7 +62,7 @@ function tick_spring(ctx, last_value, current_value, target_value) {
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
* @template [T=any]
* @param {T} [value]
* @param {SpringOpts} [opts]
* @param {SpringOptions} [opts]
* @returns {SpringStore<T>}
*/
export function spring(value, opts = {}) {
@ -83,7 +83,7 @@ export function spring(value, opts = {}) {
let cancel_task = false;
/**
* @param {T} new_value
* @param {SpringUpdateOpts} opts
* @param {SpringUpdateOptions} opts
* @returns {Promise<void>}
*/
function set(new_value, opts = {}) {
@ -191,7 +191,7 @@ export class Spring {
/**
* @param {T} value
* @param {SpringOpts} [options]
* @param {SpringOptions} [options]
*/
constructor(value, options = {}) {
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
@ -225,7 +225,7 @@ export class Spring {
* ```
* @template U
* @param {() => U} fn
* @param {SpringOpts} [options]
* @param {SpringOptions} [options]
*/
static of(fn, options) {
const spring = new Spring(fn(), options);
@ -293,7 +293,7 @@ export class Spring {
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
*
* @param {T} value
* @param {SpringUpdateOpts} [options]
* @param {SpringUpdateOptions} [options]
*/
set(value, options) {
this.#deferred?.reject(new Error('Aborted'));

@ -1,6 +1,5 @@
/** @import { Task } from '../internal/client/types' */
/** @import { Tweened } from './public' */
/** @import { TweenedOptions } from './private' */
/** @import { Tweened, TweenOptions } from './public' */
import { writable } from '../store/shared/index.js';
import { raf } from '../internal/client/timing.js';
import { loop } from '../internal/client/loop.js';
@ -84,7 +83,7 @@ function get_interpolator(a, b) {
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
* @template T
* @param {T} [value]
* @param {TweenedOptions<T>} [defaults]
* @param {TweenOptions<T>} [defaults]
* @returns {Tweened<T>}
*/
export function tweened(value, defaults = {}) {
@ -94,7 +93,7 @@ export function tweened(value, defaults = {}) {
let target_value = value;
/**
* @param {T} new_value
* @param {TweenedOptions<T>} [opts]
* @param {TweenOptions<T>} [opts]
*/
function set(new_value, opts) {
target_value = new_value;
@ -180,7 +179,7 @@ export class Tween {
#current;
#target;
/** @type {TweenedOptions<T>} */
/** @type {TweenOptions<T>} */
#defaults;
/** @type {import('../internal/client/types').Task | null} */
@ -188,7 +187,7 @@ export class Tween {
/**
* @param {T} value
* @param {TweenedOptions<T>} options
* @param {TweenOptions<T>} options
*/
constructor(value, options = {}) {
this.#current = state(value);
@ -216,7 +215,7 @@ export class Tween {
* ```
* @template U
* @param {() => U} fn
* @param {TweenedOptions<U>} [options]
* @param {TweenOptions<U>} [options]
*/
static of(fn, options) {
const tween = new Tween(fn(), options);
@ -233,7 +232,7 @@ export class Tween {
*
* If `options` are provided, they will override the tween's defaults.
* @param {T} value
* @param {TweenedOptions<T>} [options]
* @param {TweenOptions<T>} [options]
* @returns
*/
set(value, options) {

@ -1,5 +1,5 @@
/** @import { Readable } from './public' */
import { untrack } from '../index-client.js';
import { untrack } from '../internal/client/runtime.js';
import { noop } from '../internal/shared/utils.js';
/**

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

@ -0,0 +1,7 @@
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>

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

@ -0,0 +1,5 @@
<script>
import Component from "./Component.svelte";
</script>
<Component --color="red" />

@ -3,6 +3,6 @@
</script>
{#each boxes as box}
{@const area = box.width * box.height;}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target }) {
const button = /** @type {HTMLElement} */ (target.querySelector('button'));
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 0, count2: 0</p>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 1, count2: 1</p>`);
// additional tick necessary in legacy mode because it's using Promise.resolve() which finishes before the await in the component,
// causing the cache to not be set yet, which would result in count2 becoming 2
await tick();
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 2, count2: 1</p>`);
}
});

@ -0,0 +1,18 @@
<script>
let count1 = $state(0);
let count2 = $state(0);
let cache = $state({});
async function go() {
count1++;
const value = cache.value ??= await get_value();
}
function get_value() {
count2++;
return 42;
}
</script>
<button onclick={go}>go</button>
<p>count1: {count1}, count2: {count2}</p>

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target }) {
const button = /** @type {HTMLElement} */ (target.querySelector('button'));
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 0, count2: 0</p>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 1, count2: 1</p>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 2, count2: 1</p>`);
}
});

@ -0,0 +1,18 @@
<script>
let count1 = $state(0);
let count2 = $state(0);
let cache = $state({});
function go() {
count1++;
const value = cache.value ??= get_value();
}
function get_value() {
count2++;
return 42;
}
</script>
<button onclick={go}>go</button>
<p>count1: {count1}, count2: {count2}</p>

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, resolve] = target.querySelectorAll('button');
a.click();
await tick();
b.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<button>resolve</button>
hi
1
`
);
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
let b = $state(0);
let a_b = $derived(a * b);
const queued = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
queued.push(() => resolve(value));
});
}
</script>
<button onclick={() => (a++)}>a</button>
<button onclick={() => (b++)}>b</button>
<button onclick={() => (queued.shift()?.())}>resolve</button>
<!-- a_b called in a block effect before being called in an async effect -->
{#if a_b}hi{/if}
{await push(a_b)}

@ -0,0 +1,89 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift, pop] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
`
<button>1</button>
<button>shift</button>
<button>pop</button>
<p>1 = 1</p>
`
);
increment.click();
await tick();
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>3</button>
<button>shift</button>
<button>pop</button>
<p>1 = 1</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>3</button>
<button>shift</button>
<button>pop</button>
<p>1 = 1</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>3</button>
<button>shift</button>
<button>pop</button>
<p>3 = 3</p>
`
);
increment.click();
await tick();
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>5</button>
<button>shift</button>
<button>pop</button>
<p>3 = 3</p>
`
);
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>5</button>
<button>shift</button>
<button>pop</button>
<p>5 = 5</p>
`
);
}
});

@ -0,0 +1,36 @@
<script>
import { getAbortSignal } from 'svelte';
const queue = [];
function push(value) {
if (value === 1) return 1;
const d = Promise.withResolvers();
queue.push(() => d.resolve(value));
const signal = getAbortSignal();
signal.onabort = () => d.reject(signal.reason);
return d.promise;
}
function shift() {
queue.shift()?.();
}
function pop() {
queue.pop()?.();
}
let n = $state(1);
</script>
<button onclick={() => n++}>
{$state.eager(n)}
</button>
<button onclick={shift}>shift</button>
<button onclick={pop}>pop</button>
<p>{n} = {await push(n)}</p>

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const spam = /** @type {HTMLButtonElement} */ (target.querySelector('button.spam'));
const resolve = /** @type {HTMLButtonElement} */ (target.querySelector('button.resolve'));
resolve.click();
await tick();
for (let i = 0; i < 5; i += 1) {
spam.click();
await tick();
}
for (let i = 0; i < 5; i += 1) {
resolve.click();
await tick();
}
assert.equal(target.querySelectorAll('div').length, 1);
assert.htmlEqual(
target.innerHTML,
`
<button class="spam">Spam</button>
<button class="resolve">Resolve</button>
<div>5</div>
`
);
}
});

@ -0,0 +1,28 @@
<script>
let value = $state({ id: '0' });
const resolvers = [];
function wait() {
const promise = Promise.withResolvers();
resolvers.push(promise.resolve);
return promise.promise;
}
function spam() {
value.id = `${Number(value.id) + 1}`;
}
</script>
<button class="spam" onclick={spam}>Spam</button>
<button class="resolve" onclick={() => resolvers.shift()?.()}>Resolve</button>
<svelte:boundary>
{#each [value.id] as s (s)}
{await wait()}
<div>{s}</div>
{/each}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 0 - 0 - 0</button> <button>shift</button> <p>true - true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 1 - 1 - 1</button> <button>shift</button> <p>false - false</p>`
);
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const delayedCount = $derived(await push(count));
const derivedCount = $derived(count);
let resolvers = [];
function push(value) {
if (!value) return value;
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => count += 1}>
clicks: {count} - {delayedCount} - {derivedCount}
</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<p>{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}</p>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.deepEqual(logs, [10]);
btn.click();
await tick();
assert.deepEqual(logs, [10, 10]);
}
});

@ -0,0 +1,21 @@
<script>
import { fork } from 'svelte';
let s = $state(1);
let d = $derived(s * 10);
</script>
<button
onclick={() => {
const f = fork(() => {
// d has not been read yet, so this write happens with an uninitialized old value
s = 2;
d = 99;
});
f.discard();
console.log(d);
}}
>
test
</button>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<div><div><p>first test</p></div> other test</div>`);
}
});

@ -0,0 +1,14 @@
<script>
function firstTest() {
return Promise.resolve('<p>first test</p>');
}
function otherTest() {
return Promise.resolve('other test');
}
</script>
<div>
<div>{@html await firstTest()}</div>
{await otherTest()}
</div>

@ -0,0 +1,7 @@
<script>
let data = $derived(await Promise.resolve('test'));
</script>
<div data-resolved={data ? 'true' : 'false'}>
{data}
</div>

@ -0,0 +1,7 @@
<script>
import Bound from './Bound.svelte';
let open;
</script>
<Bound bind:open />

@ -0,0 +1,10 @@
import { test } from '../../test';
// Tests that renderer.subsume (which is used when bindings are present) works correctly
export default test({
mode: ['hydrate'],
html: '<div data-resolved="true">test</div>',
async test({ assert, warnings }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,7 @@
<script lang="ts">
import Async from './Async.svelte';
import Binding from './Binding.svelte';
</script>
<Async />
<Binding />

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
// if we don't skip over the never-resolving promise in the `else` block, we will never update
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 3</button><button>b 1</button><p>hello</p>`);
}
});

@ -0,0 +1,14 @@
<script>
let a = $state(0);
let b = $state(0);
let show = $state(true);
</script>
<button onclick={() => (a++, show = !show)}>a {a}</button>
<button onclick={() => (b++, show = !show)}>b {b}</button>
{#if show}
<p>hello</p>
{:else}
{await new Promise(() => {})}
{/if}

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
ssrHtml: 'works',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'works');
}
});

@ -0,0 +1,7 @@
<script lang="ts">
const test = async () => "test";
await test();
$inspect("inspect after await shouldnt break builds");
</script>
works

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'aaa 1');
}
});

@ -0,0 +1,10 @@
<script>
let name = $derived(await new Promise((a) => a('aaa')));
function use() {
return () => 1;
}
const aa = use();
</script>
{name}
{aa()}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,17 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {a}_{b}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}

@ -0,0 +1,107 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
a_c.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
b_d.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 1 | b 1 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 1 | c 1 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 2 | c 1 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
a {await delay(a)} | b {await delay(b)} | c {c} | d {d}
<button onclick={() => {a++;b++;}}>
a and b
</button>
<button onclick={() => {a++;c++;}}>
a and c
</button>
<button onclick={() => {b++;d++;}}>
b and d
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>

@ -0,0 +1,82 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
a_c.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
b_d.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
pop.click(); // second b resolved, blocked on first batch because a still pending
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
for (let i = 0; i < 3; i++) {
pop.click(); // second a resolved, first a/b now obsolete; empty queue
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 2 | c 1 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
}
}
});

@ -0,0 +1,26 @@
<script>
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
a {await delay(a)} | b {await delay(b)} | c {c} | d {d}
<button onclick={() => {a++;b++;}}>
a and b
</button>
<button onclick={() => {a++;c++;}}>
a and c
</button>
<button onclick={() => {b++;d++;}}>
b and d
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>

@ -0,0 +1,108 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
a_c.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
b_d.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click(); // first a resolved, still pending: [b, a, b]
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
pop.click(); // second b resolved, still pending: [b, a]
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click(); // first b resolved, first + last batch settled, still pending: [a]
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 1 | b 2 | c 0 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click(); // all resolved
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 2 | c 1 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
a {await delay(a)} | b {await delay(b)} | c {c} | d {d}
<button onclick={() => {a++;b++;}}>
a and b
</button>
<button onclick={() => {a++;c++;}}>
a and c
</button>
<button onclick={() => {b++;d++;}}>
b and d
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>

@ -0,0 +1,110 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
a_c.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
b_d.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click(); // first a resolved, still pending: [b, a, b]
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
pop.click(); // second b resolved, still pending: [b, a]
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
pop.click(); // second a resolved, first a/b now obsolete
// TODO would be nice to show final result here already, right now it doesn't because
// we have no handle on the already resolved first a anymore
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click(); // queue empty
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 2 | c 1 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
}
});

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

Loading…
Cancel
Save