Merge branch 'main' into effect-pre-abort

pull/16335/head
Rich Harris 1 month ago
commit f64cf42f0c

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: simplify internal component `pop()`

@ -43,6 +43,23 @@ jobs:
- run: pnpm test
env:
CI: true
TestNoAsync:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm playwright install chromium
- run: pnpm test runtime-runes
env:
CI: true
SVELTE_NO_ASYNC: true
Lint:
permissions: {}
runs-on: ubuntu-latest

@ -8,9 +8,17 @@ jobs:
trigger:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
permissions:
issues: write # to add / delete reactions
pull-requests: write # to read PR data, and to add labels
actions: read # to check workflow status
contents: read # to clone the repo
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/github-script@v6
- name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
id: check-permissions
with:
script: |
const user = context.payload.sender.login
@ -29,7 +37,7 @@ jobs:
}
if (hasTriagePermission) {
console.log('Allowed')
console.log('User is allowed. Adding +1 reaction.')
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
@ -37,16 +45,18 @@ jobs:
content: '+1',
})
} else {
console.log('Not allowed')
console.log('User is not allowed. Adding -1 reaction.')
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '-1',
})
throw new Error('not allowed')
throw new Error('User does not have the necessary permissions.')
}
- uses: actions/github-script@v6
- name: Get PR Data
uses: actions/github-script@v7
id: get-pr-data
with:
script: |
@ -59,21 +69,27 @@ jobs:
return {
num: context.issue.number,
branchName: pr.head.ref,
commit: pr.head.sha,
repo: pr.head.repo.full_name
}
- id: generate-token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0
- name: Generate Token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repository: '${{ github.repository_owner }}/svelte-ecosystem-ci'
- uses: actions/github-script@v6
app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repositories: |
svelte
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v7
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}
with:
github-token: ${{ steps.generate-token.outputs.token }}
result-encoding: string
script: |
const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }}
@ -89,6 +105,7 @@ jobs:
prNumber: '' + prData.num,
branchName: prData.branchName,
repo: prData.repo,
commit: prData.commit,
suite: suite === '' ? '-' : suite
}
})

@ -28,15 +28,6 @@ packages/svelte/types
packages/svelte/compiler/index.js
playgrounds/sandbox/src/*
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json
sites/svelte.dev/scripts/svelte-app/
sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg
sites/svelte.dev/src/routes/_components/Supporters/contributors.js
sites/svelte.dev/src/routes/_components/Supporters/donors.jpg
sites/svelte.dev/src/routes/_components/Supporters/donors.js
sites/svelte.dev/src/lib/generated
**/node_modules
**/.svelte-kit
**/.vercel

@ -17,12 +17,6 @@
"useTabs": false,
"tabWidth": 2
}
},
{
"files": ["sites/svelte-5-preview/src/routes/docs/content/**/*.md"],
"options": {
"printWidth": 60
}
}
]
}

@ -1,6 +1,3 @@
{
"search.exclude": {
"sites/svelte-5-preview/static/*": true
},
"typescript.tsdk": "node_modules/typescript/lib"
}

@ -105,10 +105,10 @@ Test samples are kept in `/test/xxx/samples` folder.
pnpm test validator
```
1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -- -t <test-name>`, for example:
1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -t <test-name>`, for example:
```bash
pnpm test validator -- -t a11y-alt-text
pnpm test validator -t a11y-alt-text
```
(You can also do `FILTER=<test-name> pnpm test <suite-name>` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.)

@ -50,7 +50,7 @@ todos.push({
});
```
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated.
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you need to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==).
Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:
@ -119,7 +119,9 @@ class Todo {
}
```
> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
### Built-in classes
Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity).
## `$state.raw`

@ -94,6 +94,27 @@ let selected = $derived(items[index]);
...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect.
## Destructuring
If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this...
```js
function stuff() { return { a: 1, b: 2, c: 3 } }
// ---cut---
let { a, b, c } = $derived(stuff());
```
...is roughly equivalent to this:
```js
function stuff() { return { a: 1, b: 2, c: 3 } }
// ---cut---
let _stuff = $derived(stuff());
let a = $derived(_stuff.a);
let b = $derived(_stuff.b);
let c = $derived(_stuff.c);
```
## Update propagation
Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull').

@ -221,6 +221,21 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
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=)):
```svelte
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<p>pending promises: {$effect.pending()}</p>
{/if}
```
## `$effect.root`
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase.

@ -0,0 +1,144 @@
---
title: await
---
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
- at the top level of your component's `<script>`
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true
}
}
};
```
The experimental flag will be removed in Svelte 6.
## Boundaries
Currently, you can only use `await` inside a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet:
```svelte
<svelte:boundary>
<MyApp />
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
## 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)...
```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>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
<p>2 + 2 = 3</p>
```
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
## Concurrency
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>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
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; }
// ---cut---
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
## Indicating loading states
In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
```js
let color = 'red';
let answer = -1;
let updating = false;
// ---cut---
import { tick, settled } from 'svelte';
async function onclick() {
updating = true;
// without this, the change to `updating` will be
// grouped with the other changes, meaning it
// won't be reflected in the UI
await tick();
color = 'octarine';
answer = 42;
await settled();
// any updates affected by `color` or `answer`
// have now been applied
updating = false;
}
```
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
Currently, server-side rendering is synchronous. If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
## Breaking changes
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).

@ -9,19 +9,41 @@ title: <svelte:boundary>
> [!NOTE]
> This feature was added in 5.3.0
Boundaries allow you to guard against errors in part of your app from breaking the app as a whole, and to recover from those errors.
Boundaries allow you to 'wall off' parts of your app, so that you can:
If an error occurs while rendering or updating the children of a `<svelte:boundary>`, or running any [`$effect`]($effect) functions contained therein, the contents will be removed.
- provide UI that should be shown when [`await`](await-expressions) expressions are first resolving
- handle errors that occur during rendering or while running effects, and provide UI that should be rendered when an error happens
Errors occurring outside the rendering process (for example, in event handlers or after a `setTimeout` or async work) are _not_ caught by error boundaries.
If a boundary handles an error (with a `failed` snippet or `onerror` handler, or both) its existing content will be removed.
> [!NOTE] Errors occurring outside the rendering process (for example, in event handlers or after a `setTimeout` or async work) are _not_ caught by error boundaries.
## Properties
For the boundary to do anything, one or both of `failed` and `onerror` must be provided.
For the boundary to do anything, one or more of the following must be provided.
### `pending`
As of Svelte 5.36, boundaries with a `pending` snippet can contain [`await`](await-expressions) expressions. This snippet will be shown when the boundary is first created, and will remain visible until all the `await` expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
```svelte
<svelte:boundary>
<p>{await delayed('hello!')}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
The `pending` snippet will _not_ be shown for subsequent async updates — for these, you can use [`$effect.pending()`]($effect#$effect.pending).
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
### `failed`
If a `failed` snippet is provided, it will be rendered with the error that was thrown, and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)):
If a `failed` snippet is provided, it will be rendered when an error is thrown inside the boundary, with the `error` and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)):
```svelte
<svelte:boundary>

@ -254,39 +254,24 @@ To declare that a variable expects the constructor or instance type of a compone
Svelte provides a best effort of all the HTML DOM types that exist. Sometimes you may want to use experimental attributes or custom events coming from an action. In these cases, TypeScript will throw a type error, saying that it does not know these types. If it's a non-experimental standard attribute/event, this may very well be a missing typing from our [HTML typings](https://github.com/sveltejs/svelte/blob/main/packages/svelte/elements.d.ts). In that case, you are welcome to open an issue and/or a PR fixing it.
In case this is a custom or experimental attribute/event, you can enhance the typings like this:
```ts
/// file: additional-svelte-typings.d.ts
declare namespace svelteHTML {
// enhance elements
interface IntrinsicElements {
'my-custom-element': { someattribute: string; 'on:event': (e: CustomEvent<any>) => void };
}
// enhance attributes
interface HTMLAttributes<T> {
// If you want to use the beforeinstallprompt event
onbeforeinstallprompt?: (event: any) => any;
// If you want to use myCustomAttribute={..} (note: all lowercase)
mycustomattribute?: any; // You can replace any with something more specific if you like
}
}
```
Then make sure that `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect.
You can also declare the typings by augmenting the `svelte/elements` module like this:
In case this is a custom or experimental attribute/event, you can enhance the typings by augmenting the `svelte/elements` module like this:
```ts
/// file: additional-svelte-typings.d.ts
import { HTMLButtonAttributes } from 'svelte/elements';
declare module 'svelte/elements' {
// add a new element
export interface SvelteHTMLElements {
'custom-button': HTMLButtonAttributes;
}
// allows for more granular control over what element to add the typings to
// add a new global attribute that is available on all html elements
export interface HTMLAttributes<T> {
globalattribute?: string;
}
// add a new attribute for button elements
export interface HTMLButtonAttributes {
veryexperimentalattribute?: string;
}
@ -294,3 +279,5 @@ declare module 'svelte/elements' {
export {}; // ensure this is not an ambient module, else types will be overridden instead of augmented
```
Then make sure that the `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect.

@ -1,5 +1,17 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### async_derived_orphan
```
Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
```
In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed.
Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them.
Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect.
### bind_invalid_checkbox_value
```
@ -68,12 +80,66 @@ Effect cannot be created inside a `$derived` value that was not itself created i
`%rune%` can only be used inside an effect (e.g. during component initialisation)
```
### effect_pending_outside_reaction
```
`$effect.pending()` can only be called inside an effect or derived
```
### effect_update_depth_exceeded
```
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
```
If an effect updates some state that it also depends on, it will re-run, potentially in a loop:
```js
let count = $state(0);
$effect(() => {
// this both reads and writes `count`,
// so will run in an infinite loop
count += 1;
});
```
(Svelte intervenes before this can crash your browser tab.)
The same applies to array mutations, since these both read and write to the array:
```js
let array = $state(['hello']);
$effect(() => {
array.push('goodbye');
});
```
Note that it's fine for an effect to re-run itself as long as it 'settles':
```js
let array = ['a', 'b', 'c'];
// ---cut---
$effect(() => {
// this is okay, because sorting an already-sorted array
// won't result in a mutation
array.sort();
});
```
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
### flush_sync_in_effect
```
Cannot use `flushSync` inside an effect
```
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### get_abort_signal_outside_reaction
```
@ -116,6 +182,14 @@ Rest element properties of `$props()` such as `%property%` are readonly
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
```
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### state_descriptors_fixed
```
@ -164,3 +238,23 @@ let odd = $derived(!even);
```
If side-effects are unavoidable, use [`$effect`]($effect) instead.
### svelte_boundary_reset_onerror
```
A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
```
If a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved.
If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick):
```svelte
<svelte:boundary onerror={async (error, reset) => {
fixTheError();
+++await tick();+++
reset();
}}>
</svelte:boundary>
```

@ -34,6 +34,86 @@ function add() {
}
```
### await_reactivity_loss
```
Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
```
Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, 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 an `await` that is not 'visible' inside the expression. In a case like this...
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
async function sum() {
return await a + b;
}
let total = $derived(await sum());
```
...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function:
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
/**
* @param {Promise<number>} a
* @param {number} b
*/
async function sum(a, b) {
return await a + b;
}
let total = $derived(await sum(a, b));
```
### await_waterfall
```
An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
```
In a case like this...
```js
async function one() { return 1 }
async function two() { return 2 }
// ---cut---
let a = $derived(await one());
let b = $derived(await two());
```
...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary.
(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.)
You can solve this by creating the promises first and _then_ awaiting them:
```js
async function one() { return 1 }
async function two() { return 2 }
// ---cut---
let aPromise = $derived(one());
let bPromise = $derived(two());
let a = $derived(await aPromise);
let b = $derived(await bPromise);
```
### binding_property_non_reactive
```
@ -232,6 +312,32 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
### svelte_boundary_reset_noop
```
A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
```
When an error occurs while rendering the contents of a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary), the `onerror` handler is called with the error plus a `reset` function that attempts to re-render the contents.
This `reset` function should only be called once. After that, it has no effect — in a case like this, where a reference to `reset` is stored outside the boundary, clicking the button while `<Contents />` is rendered will _not_ cause the contents to be rendered again.
```svelte
<script>
let reset;
</script>
<button onclick={reset}>reset</button>
<svelte:boundary onerror={(e, r) => (reset = r)}>
<!-- contents -->
{#snippet failed(e)}
<p>oops! {e.message}</p>
{/snippet}
</svelte:boundary>
```
### transition_slide_display
```

@ -480,6 +480,12 @@ Expected token %token%
Expected whitespace
```
### experimental_async
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
```
### export_undefined
```
@ -534,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of
%message%
```
### legacy_await_invalid
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
```
### legacy_export_invalid
```

@ -1,5 +1,25 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_outside_boundary
```
Cannot await outside a `<svelte:boundary>` with a `pending` snippet
```
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
### invalid_default_snippet
```

@ -49,12 +49,13 @@ export default [
},
rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/require-await': 'error',
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
// eslint is being a dummy here too
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
@ -79,7 +80,8 @@ export default [
files: ['packages/svelte/src/**/*'],
ignores: ['packages/svelte/src/compiler/**/*'],
rules: {
'custom/no_compiler_imports': 'error'
'custom/no_compiler_imports': 'error',
'svelte/no-svelte-internal': 'off'
}
},
{
@ -101,11 +103,7 @@ export default [
'*.config.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules
'sites/svelte-5-preview/**',
'tmp/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
'tmp/**'
]
}
];

@ -27,16 +27,17 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@sveltejs/eslint-config": "^8.1.0",
"@sveltejs/eslint-config": "^8.3.3",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",
"eslint-plugin-svelte": "^3.11.0",
"jsdom": "25.0.1",
"playwright": "^1.46.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.24.0",

@ -1,5 +1,143 @@
# svelte
## 5.36.13
### Patch Changes
- fix: ensure subscriptions are picked up correctly by deriveds ([#16466](https://github.com/sveltejs/svelte/pull/16466))
## 5.36.12
### Patch Changes
- chore: move `capture_signals` to legacy module ([#16456](https://github.com/sveltejs/svelte/pull/16456))
## 5.36.11
### Patch Changes
- fix: always mark reactions of deriveds ([#16457](https://github.com/sveltejs/svelte/pull/16457))
- fix: add labels to `@const` tags and props ([#16454](https://github.com/sveltejs/svelte/pull/16454))
- fix: tag stores for `$inspect.trace()` ([#16452](https://github.com/sveltejs/svelte/pull/16452))
## 5.36.10
### Patch Changes
- fix: prevent batches from getting intertwined ([#16446](https://github.com/sveltejs/svelte/pull/16446))
## 5.36.9
### Patch Changes
- fix: don't reexecute derived with no dependencies on teardown ([#16438](https://github.com/sveltejs/svelte/pull/16438))
- fix: disallow `export { foo as default }` in `<script module>` ([#16447](https://github.com/sveltejs/svelte/pull/16447))
- fix: move ownership validation into async component body ([#16449](https://github.com/sveltejs/svelte/pull/16449))
- fix: allow async destructured deriveds ([#16444](https://github.com/sveltejs/svelte/pull/16444))
- fix: move store setup/cleanup outside of async component body ([#16443](https://github.com/sveltejs/svelte/pull/16443))
## 5.36.8
### Patch Changes
- fix: keep effect in the graph if it has an abort controller ([#16430](https://github.com/sveltejs/svelte/pull/16430))
- chore: Switch `payload.out` to an array ([#16428](https://github.com/sveltejs/svelte/pull/16428))
## 5.36.7
### Patch Changes
- fix: allow instrinsic `<svelte:...>` elements to inherit from `SvelteHTMLElements` ([#16424](https://github.com/sveltejs/svelte/pull/16424))
## 5.36.6
### Patch Changes
- fix: delegate functions with shadowed variables if declared locally ([#16417](https://github.com/sveltejs/svelte/pull/16417))
- fix: handle error in correct boundary after reset ([#16171](https://github.com/sveltejs/svelte/pull/16171))
- fix: make `<svelte:boundary>` reset function a noop after the first call ([#16171](https://github.com/sveltejs/svelte/pull/16171))
## 5.36.5
### Patch Changes
- fix: silence `$inspect` errors when the effect is about to be destroyed ([#16391](https://github.com/sveltejs/svelte/pull/16391))
- fix: more informative error when effects run in an infinite loop ([#16405](https://github.com/sveltejs/svelte/pull/16405))
## 5.36.4
### Patch Changes
- fix: avoid microtask in flushSync ([#16394](https://github.com/sveltejs/svelte/pull/16394))
- fix: ensure compiler state is reset before compilation ([#16396](https://github.com/sveltejs/svelte/pull/16396))
## 5.36.3
### Patch Changes
- fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` ([#16385](https://github.com/sveltejs/svelte/pull/16385))
- fix: better handle $inspect on array mutations ([#16389](https://github.com/sveltejs/svelte/pull/16389))
- fix: leave proxied array `length` untouched when deleting properties ([#16389](https://github.com/sveltejs/svelte/pull/16389))
- fix: update `$effect.pending()` immediately after a batch is removed ([#16382](https://github.com/sveltejs/svelte/pull/16382))
## 5.36.2
### Patch Changes
- fix: add `$effect.pending()` to types ([#16376](https://github.com/sveltejs/svelte/pull/16376))
- fix: add `pending` snippet to `<svelte:boundary>` types ([#16379](https://github.com/sveltejs/svelte/pull/16379))
## 5.36.1
### Patch Changes
- fix: only skip updating bound `<input>` if the input was the source of the change ([#16373](https://github.com/sveltejs/svelte/pull/16373))
## 5.36.0
### Minor Changes
- feat: support `await` in components when using the `experimental.async` compiler option ([#15844](https://github.com/sveltejs/svelte/pull/15844))
### Patch Changes
- fix: silence a11y warning for inert elements ([#16339](https://github.com/sveltejs/svelte/pull/16339))
- chore: clean up a11y analysis code ([#16345](https://github.com/sveltejs/svelte/pull/16345))
## 5.35.7
### Patch Changes
- fix: silence autofocus a11y warning inside `<dialog>` ([#16341](https://github.com/sveltejs/svelte/pull/16341))
- fix: don't show adjusted error messages in boundaries ([#16360](https://github.com/sveltejs/svelte/pull/16360))
- chore: replace inline regex with variable ([#16340](https://github.com/sveltejs/svelte/pull/16340))
## 5.35.6
### Patch Changes
- chore: simplify reaction/source ownership tracking ([#16333](https://github.com/sveltejs/svelte/pull/16333))
- chore: simplify internal component `pop()` ([#16331](https://github.com/sveltejs/svelte/pull/16331))
## 5.35.5
### Patch Changes

@ -2080,6 +2080,7 @@ export interface SvelteHTMLElements {
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
pending?: import('svelte').Snippet;
};
[name: string]: { [name: string]: any };

@ -1,10 +1,6 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js",
"tests/**/*.ts",
"!tests/**/*.svelte",

@ -1,3 +1,13 @@
## async_derived_orphan
> Cannot create a `$derived(...)` with an `await` expression outside of an effect tree
In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed.
Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them.
Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect.
## bind_invalid_checkbox_value
> Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
@ -44,9 +54,59 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> `%rune%` can only be used inside an effect (e.g. during component initialisation)
## effect_pending_outside_reaction
> `$effect.pending()` can only be called inside an effect or derived
## effect_update_depth_exceeded
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
> Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state
If an effect updates some state that it also depends on, it will re-run, potentially in a loop:
```js
let count = $state(0);
$effect(() => {
// this both reads and writes `count`,
// so will run in an infinite loop
count += 1;
});
```
(Svelte intervenes before this can crash your browser tab.)
The same applies to array mutations, since these both read and write to the array:
```js
let array = $state(['hello']);
$effect(() => {
array.push('goodbye');
});
```
Note that it's fine for an effect to re-run itself as long as it 'settles':
```js
let array = ['a', 'b', 'c'];
// ---cut---
$effect(() => {
// this is okay, because sorting an already-sorted array
// won't result in a mutation
array.sort();
});
```
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## get_abort_signal_outside_reaction
@ -76,6 +136,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
@ -118,3 +184,21 @@ let odd = $derived(!even);
```
If side-effects are unavoidable, use [`$effect`]($effect) instead.
## svelte_boundary_reset_onerror
> A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
If a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved.
If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick):
```svelte
<svelte:boundary onerror={async (error, reset) => {
fixTheError();
+++await tick();+++
reset();
}}>
</svelte:boundary>
```

@ -30,6 +30,82 @@ function add() {
}
```
## await_reactivity_loss
> Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await`
Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, 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 an `await` that is not 'visible' inside the expression. In a case like this...
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
async function sum() {
return await a + b;
}
let total = $derived(await sum());
```
...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function:
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
/**
* @param {Promise<number>} a
* @param {number} b
*/
async function sum(a, b) {
return await a + b;
}
let total = $derived(await sum(a, b));
```
## await_waterfall
> An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app
In a case like this...
```js
async function one() { return 1 }
async function two() { return 2 }
// ---cut---
let a = $derived(await one());
let b = $derived(await two());
```
...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary.
(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.)
You can solve this by creating the promises first and _then_ awaiting them:
```js
async function one() { return 1 }
async function two() { return 2 }
// ---cut---
let aPromise = $derived(one());
let bPromise = $derived(two());
let a = $derived(await aPromise);
let b = $derived(await bPromise);
```
## binding_property_non_reactive
> `%binding%` is binding to a non-reactive property
@ -196,6 +272,30 @@ To silence the warning, ensure that `value`:
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
When an error occurs while rendering the contents of a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary), the `onerror` handler is called with the error plus a `reset` function that attempts to re-render the contents.
This `reset` function should only be called once. After that, it has no effect — in a case like this, where a reference to `reset` is stored outside the boundary, clicking the button while `<Contents />` is rendered will _not_ cause the contents to be rendered again.
```svelte
<script>
let reset;
</script>
<button onclick={reset}>reset</button>
<svelte:boundary onerror={(e, r) => (reset = r)}>
<!-- contents -->
{#snippet failed(e)}
<p>oops! {e.message}</p>
{/snippet}
</svelte:boundary>
```
## transition_slide_display
> The `slide` transition does not work correctly for elements with `display: %value%`

@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `$effect()` can only be used as an expression statement
## experimental_async
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
## export_undefined
> `%name%` is not defined
@ -98,6 +102,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> The arguments keyword cannot be used within the template or at the top level of a component
## legacy_await_invalid
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead

@ -1,3 +1,21 @@
## await_outside_boundary
> Cannot await outside a `<svelte:boundary>` with a `pending` snippet
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.35.5",
"version": "5.36.13",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -59,6 +59,9 @@
"./internal/disclose-version": {
"default": "./src/internal/disclose-version.js"
},
"./internal/flags/async": {
"default": "./src/internal/flags/async.js"
},
"./internal/flags/legacy": {
"default": "./src/internal/flags/legacy.js"
},

@ -1,5 +1,7 @@
import { DEV } from 'esm-env';
export * from '../shared/errors.js';
/**
* MESSAGE
* @param {string} PARAMETER

@ -1,3 +1,5 @@
export * from '../shared/errors.js';
/**
* MESSAGE
* @param {string} PARAMETER

@ -255,6 +255,13 @@ declare namespace $effect {
*/
export function pre(fn: () => void | (() => void)): void;
/**
* Returns the number of promises that are pending in the current boundary, not including child boundaries.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pending
*/
export function pending(): number;
/**
* 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.
*
@ -288,13 +295,13 @@ declare namespace $effect {
* let count = $state(0);
*
* const cleanup = $effect.root(() => {
* $effect(() => {
* console.log(count);
* })
* $effect(() => {
* console.log(count);
* })
*
* return () => {
* console.log('effect root cleanup');
* }
* return () => {
* console.log('effect root cleanup');
* }
* });
* </script>
*

@ -170,6 +170,15 @@ export function effect_invalid_placement(node) {
e(node, 'effect_invalid_placement', `\`$effect()\` can only be used as an expression statement\nhttps://svelte.dev/e/effect_invalid_placement`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function experimental_async(node) {
e(node, 'experimental_async', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async`);
}
/**
* `%name%` is not defined
* @param {null | number | NodeLike} node
@ -235,6 +244,15 @@ export function invalid_arguments_usage(node) {
e(node, 'invalid_arguments_usage', `The arguments keyword cannot be used within the template or at the top level of a component\nhttps://svelte.dev/e/invalid_arguments_usage`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function legacy_await_invalid(node) {
e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`);
}
/**
* Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node

@ -20,7 +20,7 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
source = remove_bom(source);
state.reset_warnings(options.warningFilter);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -63,7 +63,7 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
source = remove_bom(source);
state.reset_warnings(options.warningFilter);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_module_options(options, '');
const analysis = analyze_module(source, validated);
@ -111,7 +111,7 @@ export function compileModule(source, options) {
*/
export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
state.reset_warnings(() => false);
state.reset({ warning: () => false, filename: undefined });
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);

@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warnings } from '../state.js';
import { reset, UNKNOWN_FILENAME } from '../state.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@ -134,7 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end;
});
reset_warnings(() => false);
reset({ warning: () => false, filename });
let parsed = parse(source);
@ -145,7 +145,10 @@ export function migrate(source, { filename, use_ts } = {}) {
...validate_component_options({}, ''),
...parsed_options,
customElementOptions,
filename: filename ?? '(unknown)'
filename: filename ?? UNKNOWN_FILENAME,
experimental: {
async: true
}
};
const str = new MagicString(source);

@ -295,6 +295,8 @@ export default function element(parser) {
} else {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = create_expression_metadata();
}
if (is_top_level_script_or_style) {

@ -1,4 +1,4 @@
/** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -22,6 +22,7 @@ import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
@ -140,6 +141,7 @@ const visitors = {
AttachTag,
Attribute,
AwaitBlock,
AwaitExpression,
BindDirective,
CallExpression,
ClassBody,
@ -211,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) {
body: []
};
const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent);
const { scope, scopes, has_await } = create_scopes(
ast,
root,
allow_reactive_declarations,
parent
);
return { ast, scope, scopes };
return { ast, scope, scopes, has_await };
}
/**
@ -244,7 +251,7 @@ export function analyze_module(source, options) {
state.set_source(source);
const ast = parse(source, comments, false, false);
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -261,19 +268,19 @@ export function analyze_module(source, options) {
/** @type {Analysis} */
const analysis = {
module: { ast, scope, scopes },
module: { ast, scope, scopes, has_await },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
};
state.reset({
state.adjust({
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
});
@ -314,7 +321,12 @@ export function analyze_component(root, source, options) {
const module = js(root.module, scope_root, false, null);
const instance = js(root.instance, scope_root, true, module.scope);
const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);
const { scope, scopes, has_await } = create_scopes(
root.fragment,
scope_root,
false,
instance.scope
);
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
@ -422,7 +434,9 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
const runes =
options.runes ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) {
for (let check of synthetic_stores_legacy_check) {
@ -512,15 +526,15 @@ export function analyze_component(root, source, options) {
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
snippets: new Set(),
async_deriveds: new Set()
};
state.reset({
state.adjust({
component_name: analysis.name,
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
runes
});
if (!runes) {

@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
// If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function).
if (
local_binding !== null &&
binding !== null &&
local_binding.node !== binding.node &&
scope.declarations.get(reference) !== binding
) {
return unhoisted;
}

@ -0,0 +1,30 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
if (context.state.expression) {
context.state.expression.has_await = true;
suspend = true;
}
// disallow top-level `await` or `await` in template expressions
// unless a) in runes mode and b) opted into `experimental.async`
if (suspend) {
if (!context.state.options.experimental.async) {
e.experimental_async(node);
}
if (!context.state.analysis.runes) {
e.legacy_await_invalid(node);
}
}
context.next();
}

@ -7,6 +7,7 @@ import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
/**
* @param {CallExpression} node
@ -163,6 +164,13 @@ export function CallExpression(node, context) {
break;
case '$effect.pending':
if (context.state.expression) {
context.state.expression.has_state = true;
}
break;
case '$inspect':
if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
@ -227,7 +235,19 @@ export function CallExpression(node, context) {
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
if (rune === '$derived') {
const expression = create_expression_metadata();
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
expression
});
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
}
} else if (rune === '$inspect') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {
context.next();

@ -11,6 +11,17 @@ export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised
context.next();
if (
context.state.ast_type &&
node.specifiers.some((specifier) =>
specifier.exported.type === 'Identifier'
? specifier.exported.name === 'default'
: specifier.exported.value === 'default'
)
) {
e.module_illegal_default_export(node);
}
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (

@ -9,7 +9,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';

@ -32,6 +32,7 @@ export function StyleDirective(node, context) {
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.has_await ||= chunk.metadata.expression.has_await;
}
}
}

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
const valid = ['onerror', 'failed'];
const valid = ['onerror', 'failed', 'pending'];
/**
* @param {AST.SvelteBoundary} node

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { check_element } from './shared/a11y.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
@ -62,5 +62,17 @@ export function SvelteElement(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, parent_element: null });
context.visit(node.tag, {
...context.state,
expression: node.metadata.expression
});
for (const attribute of node.attributes) {
context.visit(attribute);
}
context.visit(node.fragment, {
...context.state,
parent_element: null
});
}

@ -3,7 +3,7 @@
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { filename } from '../../../state.js';
import { filename, UNKNOWN_FILENAME } from '../../../state.js';
/**
* @param {AST.SvelteSelf} node
@ -23,9 +23,9 @@ export function SvelteSelf(node, context) {
}
if (context.state.analysis.runes) {
const name = filename === '(unknown)' ? 'Self' : context.state.analysis.name;
const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name;
const basename =
filename === '(unknown)'
filename === UNKNOWN_FILENAME
? 'Self.svelte'
: /** @type {string} */ (filename.split(/[/\\]/).pop());

@ -0,0 +1,319 @@
/** @import { ARIARoleRelationConcept } from 'aria-query' */
import { roles as roles_map, elementRoles } from 'aria-query';
// @ts-expect-error package doesn't provide typings
import { AXObjects, elementAXObjects } from 'axobject-query';
export const aria_attributes =
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
' '
);
/** @type {Record<string, string[]>} */
export const a11y_required_attributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
// html-has-lang
html: ['lang'],
// iframe-has-title
iframe: ['title'],
img: ['alt'],
object: ['title', 'aria-label', 'aria-labelledby']
};
export const a11y_distracting_elements = ['blink', 'marquee'];
// this excludes `<a>` and `<button>` because they are handled separately
export const a11y_required_content = [
// heading-has-content
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
];
export const a11y_labelable = [
'button',
'input',
'keygen',
'meter',
'output',
'progress',
'select',
'textarea'
];
export const a11y_interactive_handlers = [
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
];
export const a11y_recommended_interactive_handlers = [
'click',
'mousedown',
'mouseup',
'keypress',
'keydown',
'keyup'
];
export const a11y_nested_implicit_semantics = new Map([
['header', 'banner'],
['footer', 'contentinfo']
]);
export const a11y_implicit_semantics = new Map([
['a', 'link'],
['area', 'link'],
['article', 'article'],
['aside', 'complementary'],
['body', 'document'],
['button', 'button'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
['dialog', 'dialog'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['figure', 'figure'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
['h3', 'heading'],
['h4', 'heading'],
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
['img', 'img'],
['li', 'listitem'],
['link', 'link'],
['main', 'main'],
['menu', 'list'],
['meter', 'progressbar'],
['nav', 'navigation'],
['ol', 'list'],
['option', 'option'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['table', 'table'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
['thead', 'rowgroup'],
['tr', 'row'],
['ul', 'list']
]);
export const menuitem_type_to_implicit_role = new Map([
['command', 'menuitem'],
['checkbox', 'menuitemcheckbox'],
['radio', 'menuitemradio']
]);
export const input_type_to_implicit_role = new Map([
['button', 'button'],
['image', 'button'],
['reset', 'button'],
['submit', 'button'],
['checkbox', 'checkbox'],
['radio', 'radio'],
['range', 'slider'],
['number', 'spinbutton'],
['email', 'textbox'],
['search', 'searchbox'],
['tel', 'textbox'],
['text', 'textbox'],
['url', 'textbox']
]);
/**
* Exceptions to the rule which follows common A11y conventions
* TODO make this configurable by the user
* @type {Record<string, string[]>}
*/
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
fieldset: ['radiogroup', 'presentation']
};
export const combobox_if_list = ['email', 'search', 'tel', 'text', 'url'];
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
export const address_type_tokens = ['shipping', 'billing'];
export const autofill_field_name_tokens = [
'',
'on',
'off',
'name',
'honorific-prefix',
'given-name',
'additional-name',
'family-name',
'honorific-suffix',
'nickname',
'username',
'new-password',
'current-password',
'one-time-code',
'organization-title',
'organization',
'street-address',
'address-line1',
'address-line2',
'address-line3',
'address-level4',
'address-level3',
'address-level2',
'address-level1',
'country',
'country-name',
'postal-code',
'cc-name',
'cc-given-name',
'cc-additional-name',
'cc-family-name',
'cc-number',
'cc-exp',
'cc-exp-month',
'cc-exp-year',
'cc-csc',
'cc-type',
'transaction-currency',
'transaction-amount',
'language',
'bday',
'bday-day',
'bday-month',
'bday-year',
'sex',
'url',
'photo'
];
export const contact_type_tokens = ['home', 'work', 'mobile', 'fax', 'pager'];
export const autofill_contact_field_name_tokens = [
'tel',
'tel-country-code',
'tel-national',
'tel-area-code',
'tel-local',
'tel-local-prefix',
'tel-local-suffix',
'tel-extension',
'email',
'impp'
];
export const ElementInteractivity = /** @type {const} */ ({
Interactive: 'interactive',
NonInteractive: 'non-interactive',
Static: 'static'
});
export const invisible_elements = ['meta', 'html', 'script', 'style'];
export const aria_roles = roles_map.keys();
export const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.includes(name));
export const non_interactive_roles = non_abstract_roles
.filter((name) => {
const role = roles_map.get(name);
return (
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
// focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable.
// 'generic' is meant to have no semantic meaning.
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
!role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
);
})
.concat(
// The `progressbar` is descended from `widget`, but in practice, its
// value is always `readonly`, so we treat it as a non-interactive role.
'progressbar'
);
export const interactive_roles = non_abstract_roles.filter(
(name) =>
!non_interactive_roles.includes(name) &&
// 'generic' is meant to have no semantic meaning.
name !== 'generic'
);
export const presentation_roles = ['presentation', 'none'];
/** @type {ARIARoleRelationConcept[]} */
export const non_interactive_element_role_schemas = [];
/** @type {ARIARoleRelationConcept[]} */
export const interactive_element_role_schemas = [];
for (const [schema, roles] of elementRoles.entries()) {
if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.includes(role))) {
non_interactive_element_role_schemas.push(schema);
}
if ([...roles].every((role) => interactive_roles.includes(role))) {
interactive_element_role_schemas.push(schema);
}
}
const interactive_ax_objects = [...AXObjects.keys()].filter(
(name) => AXObjects.get(name).type === 'widget'
);
/** @type {ARIARoleRelationConcept[]} */
export const interactive_element_ax_object_schemas = [];
/** @type {ARIARoleRelationConcept[]} */
export const non_interactive_element_ax_object_schemas = [];
const non_interactive_ax_objects = [...AXObjects.keys()].filter((name) =>
['windows', 'structure'].includes(AXObjects.get(name).type)
);
for (const [schema, ax_object] of elementAXObjects.entries()) {
if ([...ax_object].every((role) => interactive_ax_objects.includes(role))) {
interactive_element_ax_object_schemas.push(schema);
}
if ([...ax_object].every((role) => non_interactive_ax_objects.includes(role))) {
non_interactive_element_ax_object_schemas.push(schema);
}
}

@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
@ -88,6 +89,7 @@ const visitors = {
AssignmentExpression,
Attribute,
AwaitBlock,
AwaitExpression,
BinaryExpression,
BindDirective,
BlockStatement,
@ -162,6 +164,7 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
@ -206,7 +209,8 @@ export function client_component(analysis, options) {
/** @type {ESTree.Statement[]} */
const store_setup = [];
/** @type {ESTree.Statement} */
let store_init = b.empty;
/** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = [];
@ -224,8 +228,9 @@ export function client_component(analysis, options) {
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
needs_store_cleanup = true;
store_setup.push(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
store_init = b.const(
b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]),
b.call('$.setup_stores')
);
}
@ -350,7 +355,7 @@ export function client_component(analysis, options) {
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
let component_block = b.block([
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
@ -358,10 +363,64 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
.../** @type {ESTree.Statement[]} */ (template.body)
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
]);
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
if (analysis.instance.has_await) {
const params = [b.id('$$anchor')];
if (should_inject_props) {
params.push(b.id('$$props'));
}
if (store_setup.length > 0) {
params.push(b.id('$$stores'));
}
const body = b.function_declaration(
b.id('$$body'),
params,
b.block([
b.var('$$unsuspend', b.call('$.suspend')),
...component_block.body,
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body),
b.stmt(b.call('$$unsuspend'))
]),
true
);
state.hoisted.push(body);
component_block = b.block([
b.var('fragment', b.call('$.comment')),
b.var('node', b.call('$.first_child', b.id('fragment'))),
store_init,
b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))),
b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment')))
]);
} else {
component_block.body.unshift(store_init);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
// trick esrap into including comments
component_block.loc = instance.loc;
@ -392,18 +451,6 @@ export function client_component(analysis, options) {
);
}
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
// we want the cleanup function for the stores to run as the very last thing
// so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) {
@ -469,14 +516,6 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
// Merge hoisted statements into module body.
// Ensure imports are on top, with the order preserved, then module body, then hoisted statements
/** @type {ESTree.ImportDeclaration[]} */
@ -537,6 +576,10 @@ export function client_component(analysis, options) {
);
}
if (options.experimental.async) {
body.unshift(b.imports([], 'svelte/internal/flags/async'));
}
if (!analysis.runes) {
body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
}
@ -670,7 +713,9 @@ export function client_module(analysis, options) {
scopes: analysis.module.scopes,
state_fields: new Map(),
transform: {},
in_constructor: false
in_constructor: false,
in_derived: false,
is_instance: false
};
const module = /** @type {ESTree.Program} */ (

@ -21,6 +21,14 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
readonly in_derived: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;
readonly transform: Record<
string,
{
@ -41,7 +49,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
readonly store_to_invalidate?: string;
/** Stuff that happens before the render effect(s) */

@ -0,0 +1,126 @@
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
return b.call(b.await(b.call('$.save', argument)));
}
// in dev, note which values are read inside a reactive expression,
// but don't track them
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
}
return argument === node.argument ? node : { ...node, argument };
}
/**
* @param {Context} context
*/
function is_reactive_expression(context) {
if (context.state.in_derived) {
return true;
}
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {Context} context
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(context, node) {
let i = context.path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -44,10 +44,11 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (rune === '$derived') fn = b.thunk(fn);
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
return b.call('$.derived', fn);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
case '$state.snapshot':
@ -63,6 +64,9 @@ export function CallExpression(node, context) {
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
);
case '$effect.pending':
return b.call('$.pending');
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);

@ -17,7 +17,13 @@ export function ConstTag(node, context) {
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression);
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init))));
let expression = create_derived(context.state, b.thunk(init));
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.init.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
@ -55,7 +61,13 @@ export function ConstTag(node, context) {
])
);
context.state.init.push(b.const(tmp, create_derived(context.state, fn)));
let expression = create_derived(context.state, fn);
if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
context.state.init.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors

@ -312,11 +312,9 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
if (dev && node.metadata.keyed) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -326,7 +324,7 @@ export function EachBlock(node, context) {
const args = [
context.state.node,
b.literal(flags),
b.thunk(collection),
thunk,
key_function,
b.arrow(render_args, b.block(declarations.concat(block.body)))
];
@ -337,7 +335,26 @@ export function EachBlock(node, context) {
);
}
context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
if (dev && node.metadata.keyed) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
)
)
);
} else {
context.state.init.push(...statements);
}
}
/**

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

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function FunctionDeclaration(node, context) {
const state = { ...context.state, in_constructor: false };
const state = { ...context.state, in_constructor: false, in_derived: false };
if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);

@ -11,7 +11,9 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -20,7 +22,7 @@ export function HtmlTag(node, context) {
b.call(
'$.html',
context.state.node,
b.thunk(expression),
b.thunk(html),
is_svg && b.true,
is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && b.true
@ -28,5 +30,18 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push(statement);
if (node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)
);
} else {
context.state.init.push(statement);
}
}

@ -13,37 +13,32 @@ export function IfBlock(node, context) {
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const consequent_id = context.state.scope.generate('consequent');
const consequent_id = b.id(context.state.scope.generate('consequent'));
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
let alternate_id;
if (node.alternate) {
alternate_id = context.state.scope.generate('alternate');
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
const nodes = node.alternate.nodes;
let alternate_args = [b.id('$$anchor')];
if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
alternate_args.push(b.id('$$elseif'));
}
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
const test = build_expression(context, node.test, node.metadata.expression);
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.test, node.metadata.expression);
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
node.elseif ? b.id('$$anchor') : context.state.node,
context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
b.stmt(b.call('$$render', consequent_id)),
alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false)))
)
])
)
@ -71,10 +66,23 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.id('$$elseif'));
args.push(b.true);
}
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
context.state.init.push(b.block(statements));
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)
)
);
} else {
context.state.init.push(b.block(statements));
}
}

@ -11,14 +11,28 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = build_expression(context, node.expression, node.metadata.expression);
const { has_await } = node.metadata.expression;
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(
add_svelte_meta(
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
node,
'key'
)
let statement = add_svelte_meta(
b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)),
node,
'key'
);
if (has_await) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
);
}
context.state.init.push(statement);
}

@ -253,7 +253,10 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
(value, metadata) =>
metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -319,7 +322,11 @@ export function RegularElement(node, context) {
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.every(
(node) =>
node.type === 'Text' ||
(!node.metadata.expression.has_state && !node.metadata.expression.has_await)
) &&
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
@ -465,16 +472,18 @@ export function build_class_directives_object(
) {
let properties = [];
let has_call_or_state = false;
let has_await = false;
for (const d of class_directives) {
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state ? memoizer.add(directives) : directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
@ -492,6 +501,7 @@ export function build_style_directives_object(
const important = b.object([]);
let has_call_or_state = false;
let has_await = false;
for (const d of style_directives) {
const expression =
@ -503,11 +513,12 @@ export function build_style_directives_object(
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state ? memoizer.add(directives) : directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
@ -629,7 +640,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? state.memoizer.add(value) : value
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
);
const evaluated = context.state.scope.evaluate(value);

@ -1,9 +1,9 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { add_svelte_meta, build_expression } from './shared/utils.js';
import { add_svelte_meta, build_expression, Memoizer } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -12,32 +12,34 @@ import { add_svelte_meta, build_expression } from './shared/utils.js';
export function RenderTag(node, context) {
context.state.template.push_comment();
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const raw_args = expression.arguments;
const call = unwrap_optional(node.expression);
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(
build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i])
);
const { has_call } = node.metadata.arguments[i];
const memoizer = new Memoizer();
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
if (has_call) {
const id = b.id(context.state.scope.generate('render_arg'));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id)));
} else {
args.push(thunk);
let expression = build_expression(context, arg, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
}
args.push(b.thunk(expression));
}
memoizer.apply();
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
let snippet_function = build_expression(
context,
/** @type {Expression} */ (callee),
/** @type {Expression} */ (call.callee),
node.metadata.expression
);
@ -47,7 +49,7 @@ export function RenderTag(node, context) {
snippet_function = b.logical('??', snippet_function, b.id('$.noop'));
}
context.state.init.push(
statements.push(
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
@ -55,7 +57,7 @@ export function RenderTag(node, context) {
)
);
} else {
context.state.init.push(
statements.push(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
@ -67,4 +69,21 @@ export function RenderTag(node, context) {
)
);
}
const async_values = memoizer.async_values();
if (async_values) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
memoizer.async_values(),
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -1,9 +1,9 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js';
import { memoize_expression } from './shared/utils.js';
import { Memoizer } from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@ -22,7 +22,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */
const lets = [];
let is_default = true;
const memoizer = new Memoizer();
let name = b.literal('default');
@ -33,12 +33,14 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value)
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value
);
if (attribute.name === 'name') {
name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') {
if (has_state) {
props.push(b.get(attribute.name, [b.return(value)]));
@ -51,9 +53,14 @@ export function SlotElement(node, context) {
}
}
memoizer.apply();
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
const props_expression =
spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads);
@ -62,14 +69,24 @@ export function SlotElement(node, context) {
? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
'$.slot',
context.state.node,
b.id('$$props'),
name,
props_expression,
fallback
statements.push(
b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
);
context.state.init.push(b.stmt(slot));
const async_values = memoizer.async_values();
if (async_values) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -1,4 +1,4 @@
/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */
/** @import { BlockStatement, Statement, Expression, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -71,7 +71,7 @@ export function SvelteBoundary(node, context) {
hoisted.push(snippet);
if (child.expression.name === 'failed') {
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
}

@ -32,7 +32,7 @@ export function SvelteElement(node, context) {
const style_directives = [];
/** @type {ExpressionStatement[]} */
const lets = [];
const statements = [];
// Create a temporary context which picks up the init/update statements.
// They'll then be added to the function parameter of $.element
@ -64,7 +64,7 @@ export function SvelteElement(node, context) {
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state));
inner_context.state.after_update.push(b.stmt(handler));
@ -73,9 +73,6 @@ export function SvelteElement(node, context) {
}
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
if (
attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
@ -96,14 +93,10 @@ export function SvelteElement(node, context) {
);
}
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
const { has_await } = node.metadata.expression;
if (dev) {
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -123,9 +116,16 @@ export function SvelteElement(node, context) {
).body
);
if (dev) {
if (node.fragment.nodes.length > 0) {
statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const location = dev && locator(node.start);
context.state.init.push(
statements.push(
b.stmt(
b.call(
'$.element',
@ -138,4 +138,19 @@ export function SvelteElement(node, context) {
)
)
);
if (has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
@ -20,7 +20,7 @@ export function VariableDeclaration(node, context) {
if (context.state.analysis.runes) {
for (const declarator of node.declarations) {
const init = declarator.init;
const init = /** @type {Expression} */ (declarator.init);
const rune = get_rune(init, context.state.scope);
if (
@ -171,11 +171,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
return b.declarator(
id,
dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call
);
let call = b.call('$.derived', expression);
if (dev) {
const label = `[$state ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
return b.declarator(id, call);
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
@ -194,31 +197,71 @@ export function VariableDeclaration(node, context) {
}
if (rune === '$derived' || rune === '$derived.by') {
const is_async = context.state.analysis.async_deriveds.has(
/** @type {CallExpression} */ (init)
);
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(
declarator.id,
dev ? b.call('$.tag', call, b.literal(declarator.id.name)) : call
)
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
} else {
if (rune === '$derived') expression = b.thunk(expression);
let call = b.call('$.derived', expression);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let rhs = value;
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression);
rhs = b.call('$.get', id);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
call = b.call(
'$.async_derived',
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
}
if (dev) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
declarations.push(b.declarator(id, call));
}
const { inserts, paths } = extract_paths(declarator.id, rhs);
@ -228,10 +271,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
let call = b.call('$.derived', expression);
if (dev) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
declarations.push(b.declarator(id, call));
}
for (const path of paths) {

@ -4,12 +4,7 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import {
build_bind_this,
memoize_expression,
validate_binding,
add_svelte_meta
} from '../shared/utils.js';
import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -48,6 +43,8 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */
const events = {};
const memoizer = new Memoizer();
/** @type {Property[]} */
const custom_css_props = [];
@ -132,16 +129,15 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
let value = expression;
if (attribute.metadata.expression.has_call) {
const id = b.id(context.state.scope.generate('spread_element'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id);
}
props_and_spreads.push(b.thunk(value));
if (attribute.metadata.expression.has_state) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
);
} else {
props_and_spreads.push(expression);
}
@ -150,10 +146,12 @@ export function build_component(node, component_name, context) {
custom_css_props.push(
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) =>
build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block
metadata.has_call ? memoize_expression(context.state, value) : value
).value
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}).value
)
);
continue;
@ -171,20 +169,24 @@ export function build_component(node, component_name, context) {
attribute.value,
context,
(value, metadata) => {
if (!metadata.has_state) return value;
if (!metadata.has_state && !metadata.has_await) return value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component (e.g. `active={i === index}`)
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
const should_wrap_in_derived =
metadata.has_await ||
get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}
);
@ -444,7 +446,7 @@ export function build_component(node, component_name, context) {
};
}
const statements = [...snippet_declarations];
const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (is_component_dynamic) {
const prev = fn;
@ -492,5 +494,20 @@ export function build_component(node, component_name, context) {
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
}
memoizer.apply();
const async_values = memoizer.async_values();
if (async_values) {
return b.stmt(
b.call(
'$.async',
anchor,
async_values,
b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
)
);
}
return statements.length > 1 ? b.block(statements) : statements[0];
}

@ -1,4 +1,4 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
@ -33,7 +33,7 @@ export function build_attribute_effect(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoizer.add(value) : value
metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value
);
if (
@ -50,8 +50,8 @@ export function build_attribute_effect(
} else {
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
value = memoizer.add(value);
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
values.push(b.spread(value));
@ -87,6 +87,7 @@ export function build_attribute_effect(
element_id,
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
memoizer.async_values(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -118,7 +119,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return {
value: memoize(expression, chunk.metadata.expression),
has_state: chunk.metadata.expression.has_state
has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.has_await
};
}
@ -151,7 +152,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call ? context.state.memoizer.add(value) : value;
return metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata.has_await)
: value;
});
/** @type {Identifier | undefined} */
@ -165,7 +168,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
has_state ||= class_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
);
if (has_state) {
previous_id = b.id(context.state.scope.generate('classes'));
@ -219,7 +224,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? context.state.memoizer.add(value) : value
metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value
);
/** @type {Identifier | undefined} */
@ -233,7 +238,9 @@ export function build_set_style(node_id, attribute, style_directives, context) {
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
has_state ||= style_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
);
if (has_state) {
previous_id = b.id(context.state.scope.generate('styles'));

@ -9,7 +9,7 @@ import { build_hoisted_params } from '../../utils.js';
export const visit_function = (node, context) => {
const metadata = node.metadata;
let state = { ...context.state, in_constructor: false };
let state = { ...context.state, in_constructor: false, in_derived: false };
if (node.type === 'FunctionExpression') {
const parent = /** @type {Node} */ (context.path.at(-1));

@ -8,17 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
* @param {Expression} value
*/
export function memoize_expression(state, value) {
const id = b.id(state.scope.generate('expression'));
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
return b.call('$.get', id);
}
import { build_getter } from '../../utils.js';
/**
* A utility for extracting complex expressions (such as call expressions)
@ -28,19 +18,23 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#sync = [];
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/**
* @param {Expression} expression
* @param {boolean} has_await
*/
add(expression) {
add(expression, has_await) {
const id = b.id('#'); // filled in later
this.#sync.push({ id, expression });
(has_await ? this.#async : this.#sync).push({ id, expression });
return id;
}
apply() {
return this.#sync.map((memo, i) => {
return [...this.#async, ...this.#sync].map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});
@ -52,6 +46,15 @@ export class Memoizer {
);
}
async_ids() {
return this.#async.map((memo) => memo.id);
}
async_values() {
if (this.#async.length === 0) return;
return b.array(this.#async.map((memo) => b.thunk(memo.expression, true)));
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
@ -69,7 +72,8 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
) {
/** @type {Expression[]} */
const expressions = [];
@ -78,6 +82,7 @@ export function build_template_chunk(
const quasis = [quasi];
let has_state = false;
let has_await = false;
for (let i = 0; i < values.length; i++) {
const node = values[i];
@ -100,7 +105,8 @@ export function build_template_chunk(
const evaluated = state.scope.evaluate(value);
has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
has_await ||= node.metadata.expression.has_await;
has_state ||= has_await || (node.metadata.expression.has_state && !evaluated.is_known);
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
@ -156,8 +162,9 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const ids = state.memoizer.apply();
const values = state.memoizer.sync_values();
const { memoizer } = state;
const ids = memoizer.apply();
return b.stmt(
b.call(
@ -168,8 +175,8 @@ export function build_render_statement(state) {
? state.update[0].expression
: b.block(state.update)
),
values,
values && !state.analysis.runes && b.id('$.derived_safe_equal')
memoizer.sync_values(),
memoizer.async_values()
)
);
}

@ -10,6 +10,7 @@ import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
@ -44,6 +45,7 @@ import { SvelteBoundary } from './visitors/SvelteBoundary.js';
const global_visitors = {
_: set_scope,
AssignmentExpression,
AwaitExpression,
CallExpression,
ClassBody,
ExpressionStatement,

@ -0,0 +1,25 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`,
// allow it, otherwise error
if (
context.state.scope.function_depth === 0 ||
context.path.some(
(node) =>
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression'
)
) {
return context.next();
}
return b.call('$.await_outside_boundary');
}

@ -25,6 +25,10 @@ export function CallExpression(node, context) {
return b.arrow([], b.block([]));
}
if (rune === '$effect.pending') {
return b.literal(0);
}
if (rune === '$state' || rune === '$state.raw') {
return node.arguments[0] ? context.visit(node.arguments[0]) : b.void0;
}

@ -44,12 +44,12 @@ export function EachBlock(node, context) {
);
if (node.fallback) {
const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
const open = b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open));
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push(

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

@ -3,15 +3,36 @@
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(
b.literal(BLOCK_OPEN),
/** @type {BlockStatement} */ (context.visit(node.fragment)),
b.literal(BLOCK_CLOSE)
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
);
const pending_snippet = /** @type {AST.SnippetBlock} */ (
node.fragment.nodes.find(
(node) => node.type === 'SnippetBlock' && node.expression.name === 'pending'
)
);
if (pending_attribute) {
const value = build_attribute_value(pending_attribute.value, context, false, true);
context.state.template.push(b.call(value, b.id('$$payload')));
} else if (pending_snippet) {
context.state.template.push(
/** @type {BlockStatement} */ (context.visit(pending_snippet.body))
);
} else {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
}
context.state.template.push(b.literal(BLOCK_CLOSE));
}

@ -96,10 +96,10 @@ function is_statement(node) {
/**
* @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator} operator
* @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]}
*/
export function build_template(template, out = b.id('$$payload.out'), operator = '+=') {
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') {
/** @type {string[]} */
let strings = [];
@ -110,18 +110,32 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = [];
const flush = () => {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
if (operator === 'push') {
statements.push(
b.stmt(
b.call(
b.member(out, b.id('push')),
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
)
)
);
);
} else {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
)
);
}
strings = [];
expressions = [];
};

@ -66,7 +66,8 @@ export function create_expression_metadata() {
has_state: false,
has_call: false,
has_member_expression: false,
has_assignment: false
has_assignment: false,
has_await: false
};
}

@ -23,3 +23,5 @@ export const regex_heading_tags = /^h[1-6]$/;
export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/;
export const regex_bidirectional_control_characters =
/[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g;
export const regex_js_prefix = /^\W*javascript:/i;
export const regex_redundant_img_alt = /\b(image|picture|photo)\b/i;

@ -18,9 +18,9 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
export const FUNCTION = Symbol('string');
const NUMBER = Symbol('number');
const STRING = Symbol('string');
const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
const globals = {
@ -933,7 +933,25 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
};
let has_await = false;
walk(ast, state, {
AwaitExpression(node, context) {
// this doesn't _really_ belong here, but it allows us to
// automatically opt into runes mode on encountering
// blocking awaits, without doing an additional walk
// before the analysis occurs
// TODO remove this in Svelte 7.0 or whenever we get rid of legacy support
has_await ||= context.path.every(
({ type }) =>
type !== 'ArrowFunctionExpression' &&
type !== 'FunctionExpression' &&
type !== 'FunctionDeclaration'
);
context.next();
},
// references
Identifier(node, { path, state }) {
const parent = path.at(-1);
@ -1290,6 +1308,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
return {
has_await,
scope,
scopes
};

@ -1,21 +1,19 @@
import type { AST, Binding, StateField } from '#compiler';
import type {
AssignmentExpression,
CallExpression,
ClassBody,
Comment,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { StateCreationRuneName } from '../../utils.js';
import type { AnalysisState } from './2-analyze/types.js';
export interface Js {
ast: Program;
scope: Scope;
scopes: Map<AST.SvelteNode, Scope>;
has_await: boolean;
}
export interface Template {
@ -46,6 +44,9 @@ export interface Analysis {
// TODO figure out if we can move this to ComponentAnalysis
accessors: boolean;
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
}
export interface ComponentAnalysis extends Analysis {

@ -16,6 +16,11 @@ export let warnings = [];
*/
export let filename;
/**
* This is the fallback used when no filename is specified.
*/
export const UNKNOWN_FILENAME = '(unknown)';
/**
* The name of the component that is used in the `export default function ...` statement.
*/
@ -80,15 +85,6 @@ export function pop_ignore() {
ignore_stack.pop();
}
/**
*
* @param {(warning: Warning) => boolean} fn
*/
export function reset_warnings(fn = () => true) {
warning_filter = fn;
warnings = [];
}
/**
* @param {AST.SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
@ -99,21 +95,36 @@ export function is_ignored(node, code) {
}
/**
* Call this to reset the compiler state. Should be called before each compilation.
* @param {{ warning?: (warning: Warning) => boolean; filename: string | undefined }} state
*/
export function reset(state) {
dev = false;
runes = false;
component_name = UNKNOWN_FILENAME;
source = '';
locator = () => undefined;
filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/');
warning_filter = state.warning ?? (() => true);
warnings = [];
}
/**
* Adjust the compiler state based on the provided state object.
* Call this after parsing and basic analysis happened.
* @param {{
* dev: boolean;
* filename: string;
* component_name?: string;
* rootDir?: string;
* runes: boolean;
* }} state
*/
export function reset(state) {
export function adjust(state) {
const root_dir = state.rootDir?.replace(/\\/g, '/');
filename = state.filename.replace(/\\/g, '/');
dev = state.dev;
runes = state.runes;
component_name = state.component_name ?? '(unknown)';
component_name = state.component_name ?? UNKNOWN_FILENAME;
if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir

@ -224,6 +224,17 @@ export interface ModuleCompileOptions {
* Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it.
*/
warningFilter?: (warning: Warning) => boolean;
/**
* Experimental options
* @since 5.36
*/
experimental?: {
/**
* Allow `await` keyword in deriveds, template expressions, and the top level of components
* @since 5.36
*/
async?: boolean;
};
}
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still
@ -287,6 +298,8 @@ export interface ExpressionMetadata {
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression contains `await` */
has_await: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */

@ -366,6 +366,7 @@ export namespace AST {
tag: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/**
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.

@ -1,4 +1,5 @@
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
@ -30,16 +31,17 @@ export function assignment_pattern(left, right) {
/**
* @param {Array<ESTree.Pattern>} params
* @param {ESTree.BlockStatement | ESTree.Expression} body
* @param {boolean} async
* @returns {ESTree.ArrowFunctionExpression}
*/
export function arrow(params, body) {
export function arrow(params, body, async = false) {
return {
type: 'ArrowFunctionExpression',
params,
body,
expression: body.type !== 'BlockStatement',
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}
@ -100,7 +102,7 @@ export function labeled(name, body) {
/**
* @param {string | ESTree.Expression} callee
* @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined)} args
* @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined | null)} args
* @returns {ESTree.CallExpression}
*/
export function call(callee, ...args) {
@ -216,16 +218,17 @@ export function export_default(declaration) {
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
* @param {ESTree.BlockStatement} body
* @param {boolean} async
* @returns {ESTree.FunctionDeclaration}
*/
export function function_declaration(id, params, body) {
export function function_declaration(id, params, body, async = false) {
return {
type: 'FunctionDeclaration',
id,
params,
body,
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}
@ -421,19 +424,32 @@ export function template(elements, expressions) {
* @returns {ESTree.Expression}
*/
export function thunk(expression, async = false) {
const fn = arrow([], expression);
if (async) fn.async = true;
return unthunk(fn);
return unthunk(arrow([], expression, async));
}
/**
* Replace "(arg) => func(arg)" to "func"
* @param {ESTree.Expression} expression
* @param {ESTree.ArrowFunctionExpression} expression
* @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') {
let has_await = false;
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
if (
expression.type === 'ArrowFunctionExpression' &&
expression.async === false &&
expression.body.type === 'CallExpression' &&
expression.body.callee.type === 'Identifier' &&

@ -61,7 +61,7 @@ export class CompileDiagnostic {
this.code = code;
this.message = message;
if (state.filename) {
if (state.filename !== state.UNKNOWN_FILENAME) {
this.filename = state.filename;
}

@ -41,7 +41,11 @@ const common = {
return input;
}),
warningFilter: fun(() => true)
warningFilter: fun(() => true),
experimental: object({
async: boolean(false)
})
};
export const validate_module_options =
@ -241,19 +245,6 @@ function validator(fallback, fn) {
};
}
/**
* @param {number} fallback
* @returns {Validator}
*/
function number(fallback) {
return validator(fallback, (input, keypath) => {
if (typeof input !== 'number') {
throw_error(`${keypath} should be a number, if specified`);
}
return input;
});
}
/**
* @param {string | undefined} fallback
* @param {boolean} allow_empty
@ -273,20 +264,6 @@ function string(fallback, allow_empty = true) {
});
}
/**
* @param {string[]} fallback
* @returns {Validator}
*/
function string_array(fallback) {
return validator(fallback, (input, keypath) => {
if (input && !Array.isArray(input)) {
throw_error(`${keypath} should be a string array, if specified`);
}
return input;
});
}
/**
* @param {boolean | undefined} fallback
* @returns {Validator}

@ -42,6 +42,8 @@ export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
// we use a list of ignorable runtime warnings because not every runtime warning
// can be ignored and we want to keep the validation for svelte-ignore in place
export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'await_waterfall',
'await_reactivity_loss',
'state_snapshot_uncloneable',
'binding_property_non_reactive',
'hydration_attribute_changed',

@ -5,7 +5,6 @@ import { active_reaction, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
import { legacy_mode_flag } from './internal/flags/index.js';
import { component_context } from './internal/client/context.js';
import { DEV } from 'esm-env';
@ -91,7 +90,7 @@ export function getAbortSignal() {
*/
export function onMount(fn) {
if (component_context === null) {
lifecycle_outside_component('onMount');
e.lifecycle_outside_component('onMount');
}
if (legacy_mode_flag && component_context.l !== null) {
@ -115,7 +114,7 @@ export function onMount(fn) {
*/
export function onDestroy(fn) {
if (component_context === null) {
lifecycle_outside_component('onDestroy');
e.lifecycle_outside_component('onDestroy');
}
onMount(() => () => untrack(fn));
@ -158,7 +157,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
export function createEventDispatcher() {
const active_component_context = component_context;
if (active_component_context === null) {
lifecycle_outside_component('createEventDispatcher');
e.lifecycle_outside_component('createEventDispatcher');
}
return (type, detail, options) => {
@ -196,7 +195,7 @@ export function createEventDispatcher() {
*/
export function beforeUpdate(fn) {
if (component_context === null) {
lifecycle_outside_component('beforeUpdate');
e.lifecycle_outside_component('beforeUpdate');
}
if (component_context.l === null) {
@ -219,7 +218,7 @@ export function beforeUpdate(fn) {
*/
export function afterUpdate(fn) {
if (component_context === null) {
lifecycle_outside_component('afterUpdate');
e.lifecycle_outside_component('afterUpdate');
}
if (component_context.l === null) {
@ -238,8 +237,8 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
export { flushSync } from './internal/client/runtime.js';
export { flushSync } from './internal/client/reactivity/batch.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -35,6 +35,8 @@ export function unmount() {
export async function tick() {}
export async function settled() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';

@ -18,8 +18,13 @@ export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const EFFECT_IS_UPDATING = 1 << 20;
export const USER_EFFECT = 1 << 21;
export const USER_EFFECT = 1 << 20;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;
export const ERROR_VALUE = 1 << 23;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');

@ -1,17 +1,11 @@
/** @import { ComponentContext, DevStackEntry } from '#client' */
/** @import { ComponentContext, DevStackEntry, Effect } from '#client' */
import { DEV } from 'esm-env';
import { lifecycle_outside_component } from '../shared/errors.js';
import { source } from './reactivity/sources.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from './runtime.js';
import { create_user_effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
import * as e from './errors.js';
import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
@ -103,6 +97,16 @@ export function getContext(key) {
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f;
var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
if (!valid) {
e.set_context_after_init();
}
}
context_map.set(key, context);
return context;
}
@ -145,18 +149,9 @@ export function push(props, runes = false, fn) {
e: null,
s: props,
x: null,
l: null
l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null
};
if (legacy_mode_flag && !runes) {
component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
component_context.function = fn;
@ -205,7 +200,7 @@ export function is_runes() {
*/
function get_or_init_context_map(name) {
if (component_context === null) {
lifecycle_outside_component(name);
e.lifecycle_outside_component(name);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));

@ -7,6 +7,7 @@ import {
CLEAN,
DERIVED,
EFFECT,
ASYNC,
MAYBE_DIRTY,
RENDER_EFFECT,
ROOT_EFFECT
@ -39,6 +40,8 @@ export function log_effect_tree(effect, depth = 0) {
label = 'boundary';
} else if ((flags & BLOCK_EFFECT) !== 0) {
label = 'block';
} else if ((flags & ASYNC) !== 0) {
label = 'async';
} else if ((flags & BRANCH_EFFECT) !== 0) {
label = 'branch';
} else if ((flags & RENDER_EFFECT) !== 0) {
@ -60,6 +63,13 @@ export function log_effect_tree(effect, depth = 0) {
// eslint-disable-next-line no-console
console.log(callsite);
} else {
// eslint-disable-next-line no-console
console.groupCollapsed(`%cfn`, `font-weight: normal`);
// eslint-disable-next-line no-console
console.log(effect.fn);
// eslint-disable-next-line no-console
console.groupEnd();
}
if (effect.deps !== null) {

@ -1,6 +1,6 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
/**
@ -12,29 +12,44 @@ export function inspect(get_value, inspector = console.log) {
validate_effect('$inspect');
let initial = true;
let error = /** @type {any} */ (UNINITIALIZED);
// Inspect effects runs synchronously so that we can capture useful
// stack traces. As a consequence, reading the value might result
// in an error (an `$inspect(object.property)` will run before the
// `{#if object}...{/if}` that contains it)
inspect_effect(() => {
/** @type {any} */
var value = UNINITIALIZED;
// Capturing the value might result in an exception due to the inspect effect being
// sync and thus operating on stale data. In the case we encounter an exception we
// can bail-out of reporting the value. Instead we simply console.error the error
// so at least it's known that an error occured, but we don't stop execution
try {
value = get_value();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
var value = get_value();
} catch (e) {
error = e;
return;
}
if (value !== UNINITIALIZED) {
var snap = snapshot(value, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
});
}
var snap = snapshot(value, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
});
initial = false;
});
// If an error occurs, we store it (along with its stack trace).
// If the render effect subsequently runs, we log the error,
// but if it doesn't run it's because the `$inspect` was
// destroyed, meaning we don't need to bother
render_effect(() => {
try {
// call `get_value` so that this runs alongside the inspect effect
get_value();
} catch {
// ignore
}
if (error !== UNINITIALIZED) {
// eslint-disable-next-line no-console
console.error(error);
error = UNINITIALIZED;
}
});
}

@ -2,9 +2,9 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
import { active_reaction, untrack } from '../runtime.js';
/**
* @typedef {{
@ -26,7 +26,7 @@ function log_entry(signal, entry) {
return;
}
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const type = get_type(signal);
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty
@ -56,8 +56,10 @@ function log_entry(signal, entry) {
}
if (dirty && signal.updated) {
// eslint-disable-next-line no-console
console.log(signal.updated);
for (const updated of signal.updated.values()) {
// eslint-disable-next-line no-console
console.log(updated.error);
}
}
if (entry) {
@ -71,6 +73,15 @@ function log_entry(signal, entry) {
console.groupEnd();
}
/**
* @param {Value} signal
* @returns {'$state' | '$derived' | 'store'}
*/
function get_type(signal) {
if ((signal.f & (DERIVED | ASYNC)) !== 0) return '$derived';
return signal.label?.startsWith('$') ? 'store' : '$state';
}
/**
* @template T
* @param {() => string} label
@ -120,44 +131,46 @@ export function trace(label, fn) {
/**
* @param {string} label
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
let error = Error();
const stack = error.stack;
if (stack) {
const lines = stack.split('\n');
const new_lines = ['\n'];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
continue;
}
new_lines.push(line);
}
if (!stack) return null;
if (new_lines.length === 1) {
const lines = stack.split('\n');
const new_lines = ['\n'];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
continue;
}
new_lines.push(line);
}
define_property(error, 'stack', {
value: new_lines.join('\n')
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
});
if (new_lines.length === 1) {
return null;
}
return error;
define_property(error, 'stack', {
value: new_lines.join('\n')
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
});
return /** @type {Error & { stack: string }} */ (error);
}
/**

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

@ -0,0 +1,26 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import { get_pending_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
var boundary = get_pending_boundary();
boundary.update_pending_count(1);
flatten([], expressions, (values) => {
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
fn(node, ...values);
} finally {
boundary.update_pending_count(-1);
}
});
}

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { set_active_effect, set_active_reaction } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
@ -22,6 +22,7 @@ import {
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
import { flushSync } from '../../reactivity/batch.js';
const PENDING = 0;
const THEN = 1;

@ -1,11 +1,17 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BOUNDARY_EFFECT,
EFFECT_PRESERVED,
EFFECT_RAN,
EFFECT_TRANSPARENT
} from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
@ -17,12 +23,21 @@ import {
remove_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
/**
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void;
* }} BoundaryProps
*/
@ -39,6 +54,11 @@ export function boundary(node, props, children) {
}
export class Boundary {
pending = false;
/** @type {Boundary | null} */
parent;
/** @type {TemplateNode} */
#anchor;
@ -57,11 +77,45 @@ export class Boundary {
/** @type {Effect | null} */
#main_effect = null;
/** @type {Effect | null} */
#pending_effect = null;
/** @type {Effect | null} */
#failed_effect = null;
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
#pending_count = 0;
#is_creating_fallback = false;
/**
* A source containing the number of pending async deriveds/expressions.
* Only created if `$effect.pending()` is used inside the boundary,
* otherwise updating the source results in needless `Batch.ensure()`
* calls followed by no-op flushes
* @type {Source<number> | null}
*/
#effect_pending = null;
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count);
if (DEV) {
tag(this.#effect_pending, '$effect.pending()');
}
return () => {
this.#effect_pending = null;
};
});
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
@ -74,6 +128,10 @@ export class Boundary {
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b;
this.pending = !!this.#props.pending;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
@ -81,10 +139,43 @@ export class Boundary {
hydrate_next();
}
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
const pending = this.#props.pending;
if (hydrating && pending) {
this.#pending_effect = branch(() => pending(this.#anchor));
// future work: when we have some form of async SSR, we will
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.pending = false;
}
});
} else {
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
}
}
}, flags);
@ -93,6 +184,10 @@ export class Boundary {
}
}
has_pending_snippet() {
return !!this.#props.pending;
}
/**
* @param {() => Effect | null} fn
*/
@ -107,6 +202,9 @@ export class Boundary {
try {
return fn();
} catch (e) {
handle_error(e);
return null;
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
@ -114,22 +212,116 @@ export class Boundary {
}
}
#show_pending_snippet() {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
move_effect(this.#main_effect, this.#offscreen_fragment);
}
if (this.#pending_effect === null) {
this.#pending_effect = branch(() => pending(this.#anchor));
}
}
/** @param {1 | -1} d */
#update_pending_count(d) {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.pending = false;
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
this.#pending_effect = null;
});
}
if (this.#offscreen_fragment) {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
}
}
/** @param {1 | -1} d */
update_pending_count(d) {
if (this.has_pending_snippet()) {
this.#update_pending_count(d);
} else if (this.parent) {
this.parent.#update_pending_count(d);
}
effect_pending_updates.add(this.#effect_pending_update);
}
get_effect_pending() {
this.#effect_pending_subscriber();
return get(/** @type {Source<number>} */ (this.#effect_pending));
}
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (this.#pending_effect) {
destroy_effect(this.#pending_effect);
this.#pending_effect = null;
}
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
var did_reset = false;
var calling_on_error = false;
const reset = () => {
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
}
did_reset = true;
if (calling_on_error) {
e.svelte_boundary_reset_onerror();
}
this.#pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
this.pending = true;
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
}
};
// If we have nothing to capture the error, or if we hit an error while
@ -142,27 +334,15 @@ export class Boundary {
try {
set_active_reaction(null);
calling_on_error = true;
onerror?.(error, reset);
calling_on_error = false;
} catch (error) {
invoke_error_boundary(error, this.#effect && this.#effect.parent);
} finally {
set_active_reaction(previous_reaction);
}
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
@ -187,3 +367,49 @@ export class Boundary {
}
}
}
/**
*
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
function move_effect(effect, fragment) {
var node = effect.nodes_start;
var end = effect.nodes_end;
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
fragment.append(node);
node = next;
}
}
export function get_pending_boundary() {
var boundary = /** @type {Effect} */ (active_effect).b;
while (boundary !== null && !boundary.has_pending_snippet()) {
boundary = boundary.parent;
}
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
}
export function pending() {
if (active_effect === null) {
e.effect_pending_outside_reaction();
}
var boundary = active_effect.b;
if (boundary === null) {
return 0; // TODO eventually we will need this to be global
}
return boundary.get_effect_pending();
}

@ -1,4 +1,5 @@
/** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@ -21,7 +22,8 @@ import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling
get_next_sibling,
should_defer_append
} from '../operations.js';
import {
block,
@ -39,6 +41,7 @@ import { queue_micro_task } from '../task.js';
import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -66,9 +69,10 @@ export function index(_, i) {
* @param {EachState} state
* @param {EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {Map<any, EachItem>} items_map
*/
function pause_effects(state, items, controlled_anchor, items_map) {
function pause_effects(state, items, controlled_anchor) {
var items_map = state.items;
/** @type {TransitionManager[]} */
var transitions = [];
var length = items.length;
@ -137,6 +141,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var was_empty = false;
/** @type {Map<any, EachItem>} */
var offscreen_items = new Map();
// TODO: ideally we could use derived for runes mode but because of the ability
// to use a store which can be mutated, we can't do that here as mutating a store
// will still result in the collection array being the same from the store
@ -146,8 +153,45 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
return is_array(collection) ? collection : collection == null ? [] : array_from(collection);
});
/** @type {V[]} */
var array;
/** @type {Effect} */
var each_effect;
function commit() {
reconcile(
each_effect,
array,
state,
offscreen_items,
anchor,
render_fn,
flags,
get_key,
get_collection
);
if (fallback_fn !== null) {
if (array.length === 0) {
if (fallback) {
resume_effect(fallback);
} else {
fallback = branch(() => fallback_fn(anchor));
}
} else if (fallback !== null) {
pause_effect(fallback, () => {
fallback = null;
});
}
}
}
block(() => {
var array = get(each_array);
// store a reference to the effect so that we can update the start/end nodes in reconciliation
each_effect ??= /** @type {Effect} */ (active_effect);
array = get(each_array);
var length = array.length;
if (was_empty && length === 0) {
@ -219,21 +263,56 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
}
if (!hydrating) {
reconcile(array, state, anchor, render_fn, flags, get_key, get_collection);
}
if (hydrating) {
if (length === 0 && fallback_fn) {
fallback = branch(() => fallback_fn(anchor));
}
} else {
if (should_defer_append()) {
var keys = new Set();
var batch = /** @type {Batch} */ (current_batch);
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
var existing = state.items.get(key) ?? offscreen_items.get(key);
if (existing) {
// update before reconciliation, to trigger any async updates
if ((flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0) {
update_item(existing, value, i, flags);
}
} else {
item = create_item(
null,
state,
null,
null,
value,
key,
i,
render_fn,
flags,
get_collection,
true
);
offscreen_items.set(key, item);
}
if (fallback_fn !== null) {
if (length === 0) {
if (fallback) {
resume_effect(fallback);
} else {
fallback = branch(() => fallback_fn(anchor));
keys.add(key);
}
} else if (fallback !== null) {
pause_effect(fallback, () => {
fallback = null;
});
for (const [key, item] of state.items) {
if (!keys.has(key)) {
batch.skipped_effects.add(item.e);
}
}
batch.add_callback(commit);
} else {
commit();
}
}
@ -259,8 +338,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
/**
* Add, remove, or reorder items output by an each block as its input changes
* @template V
* @param {Effect} each_effect
* @param {Array<V>} array
* @param {EachState} state
* @param {Map<any, EachItem>} offscreen_items
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
@ -268,7 +349,17 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
* @param {() => V[]} get_collection
* @returns {void}
*/
function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) {
function reconcile(
each_effect,
array,
state,
offscreen_items,
anchor,
render_fn,
flags,
get_key,
get_collection
) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
@ -320,23 +411,39 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (item === undefined) {
var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor;
prev = create_item(
child_anchor,
state,
prev,
prev === null ? state.first : prev.next,
value,
key,
i,
render_fn,
flags,
get_collection
);
var pending = offscreen_items.get(key);
if (pending !== undefined) {
offscreen_items.delete(key);
items.set(key, pending);
var next = prev ? prev.next : current;
link(state, prev, pending);
link(state, pending, next);
move(pending, next, anchor);
prev = pending;
} else {
var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor;
prev = create_item(
child_anchor,
state,
prev,
prev === null ? state.first : prev.next,
value,
key,
i,
render_fn,
flags,
get_collection
);
}
items.set(key, prev);
@ -455,7 +562,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
}
}
pause_effects(state, to_destroy, controlled_anchor, items);
pause_effects(state, to_destroy, controlled_anchor);
}
}
@ -468,8 +575,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti
});
}
/** @type {Effect} */ (active_effect).first = state.first && state.first.e;
/** @type {Effect} */ (active_effect).last = prev && prev.e;
each_effect.first = state.first && state.first.e;
each_effect.last = prev && prev.e;
for (var unused of offscreen_items.values()) {
destroy_effect(unused.e);
}
offscreen_items.clear();
}
/**
@ -493,7 +606,7 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {Node | null} anchor
* @param {EachState} state
* @param {EachItem | null} prev
* @param {EachItem | null} next
@ -503,6 +616,7 @@ function update_item(item, value, index, type) {
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @param {boolean} [deferred]
* @returns {EachItem}
*/
function create_item(
@ -515,7 +629,8 @@ function create_item(
index,
render_fn,
flags,
get_collection
get_collection,
deferred
) {
var previous_each_item = current_each_item;
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
@ -549,13 +664,20 @@ function create_item(
current_each_item = item;
try {
item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating);
if (anchor === null) {
var fragment = document.createDocumentFragment();
fragment.append((anchor = create_text()));
}
item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating);
item.e.prev = prev && prev.e;
item.e.next = next && next.e;
if (prev === null) {
state.first = item;
if (!deferred) {
state.first = item;
}
} else {
prev.next = item;
prev.e.next = item.e;
@ -583,7 +705,7 @@ function move(item, next, anchor) {
var dest = next ? /** @type {TemplateNode} */ (next.e.nodes_start) : anchor;
var node = /** @type {TemplateNode} */ (item.e.nodes_start);
while (node !== end) {
while (node !== null && node !== end) {
var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
dest.before(node);
node = next_node;

@ -1,4 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
@ -10,16 +11,20 @@ import {
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn
* @param {[number,number]} [elseif]
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && root_index === 0) {
export function if_block(node, fn, elseif = false) {
if (hydrating) {
hydrate_next();
}
@ -34,45 +39,56 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
/** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0;
var flags = elseif ? EFFECT_TRANSPARENT : 0;
var has_branch = false;
const set_branch = (
/** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn,
flag = true
) => {
const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
};
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
function commit() {
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) {
resume_effect(active);
}
if (inactive) {
pause_effect(inactive, () => {
if (condition) {
alternate_effect = null;
} else {
consequent_effect = null;
}
});
}
}
const update_branch = (
/** @type {boolean | null} */ new_condition,
/** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn
/** @type {null | ((anchor: Node) => void)} */ fn
) => {
if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
if (hydrating && hydrate_index !== -1) {
if (root_index === 0) {
const data = read_hydration_instruction(anchor);
if (data === HYDRATION_START) {
hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) {
hydrate_index = Infinity;
} else {
hydrate_index = parseInt(data.substring(1));
if (hydrate_index !== hydrate_index) {
// if hydrate_index is NaN
// we set an invalid index to force mismatch
hydrate_index = condition ? Infinity : -1;
}
}
}
const is_else = hydrate_index > root_index;
if (hydrating) {
const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
@ -82,34 +98,35 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
hydrate_index = -1; // ignore hydration in next else if
}
}
if (condition) {
if (consequent_effect) {
resume_effect(consequent_effect);
} else if (fn) {
consequent_effect = branch(() => fn(anchor));
}
var defer = should_defer_append();
var target = anchor;
if (alternate_effect) {
pause_effect(alternate_effect, () => {
alternate_effect = null;
});
}
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
if (condition) {
consequent_effect ??= fn && branch(() => fn(target));
} else {
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (fn) {
alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index]));
}
alternate_effect ??= fn && branch(() => fn(target));
}
if (consequent_effect) {
pause_effect(consequent_effect, () => {
consequent_effect = null;
});
}
if (defer) {
var batch = /** @type {Batch} */ (current_batch);
var active = condition ? consequent_effect : alternate_effect;
var inactive = condition ? alternate_effect : consequent_effect;
if (active) batch.skipped_effects.delete(active);
if (inactive) batch.skipped_effects.add(inactive);
batch.add_callback(commit);
} else {
commit();
}
if (mismatch) {

@ -1,9 +1,12 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
import { is_runes } from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
import { current_batch } from '../../reactivity/batch.js';
/**
* @template V
@ -12,7 +15,7 @@ import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
* @param {(anchor: Node) => TemplateNode | void} render_fn
* @returns {void}
*/
export function key_block(node, get_key, render_fn) {
export function key(node, get_key, render_fn) {
if (hydrating) {
hydrate_next();
}
@ -25,15 +28,48 @@ export function key_block(node, get_key, render_fn) {
/** @type {Effect} */
var effect;
/** @type {Effect} */
var pending_effect;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
var changed = is_runes() ? not_equal : safe_not_equal;
function commit() {
if (effect) {
pause_effect(effect);
}
if (offscreen_fragment !== null) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
}
block(() => {
if (changed(key, (key = get_key()))) {
if (effect) {
pause_effect(effect);
var target = anchor;
var defer = should_defer_append();
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
effect = branch(() => render_fn(anchor));
pending_effect = branch(() => render_fn(target));
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
}
});

@ -1,7 +1,10 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { current_batch } from '../../reactivity/batch.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_text, should_defer_append } from '../operations.js';
/**
* @template P
@ -24,16 +27,50 @@ export function component(node, get_component, render_fn) {
/** @type {Effect | null} */
var effect;
block(() => {
if (component === (component = get_component())) return;
/** @type {DocumentFragment | null} */
var offscreen_fragment = null;
/** @type {Effect | null} */
var pending_effect = null;
function commit() {
if (effect) {
pause_effect(effect);
effect = null;
}
if (offscreen_fragment) {
// remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove();
anchor.before(offscreen_fragment);
offscreen_fragment = null;
}
effect = pending_effect;
pending_effect = null;
}
block(() => {
if (component === (component = get_component())) return;
var defer = should_defer_append();
if (component) {
effect = branch(() => render_fn(anchor, component));
var target = anchor;
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
}
pending_effect = branch(() => render_fn(target, component));
}
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
}, EFFECT_TRANSPARENT);

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

Loading…
Cancel
Save