Merge branch 'main' into $state-invalidate

pull/15673/head
ComputerGuy 1 month ago committed by GitHub
commit 256c7966aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reset attribute cache after setting corresponding property

@ -43,6 +43,23 @@ jobs:
- run: pnpm test - run: pnpm test
env: env:
CI: true 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: Lint:
permissions: {} permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest

@ -8,9 +8,17 @@ jobs:
trigger: trigger:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
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: steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: monitor action permissions
- uses: actions/github-script@v6 uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
id: check-permissions
with: with:
script: | script: |
const user = context.payload.sender.login const user = context.payload.sender.login
@ -29,7 +37,7 @@ jobs:
} }
if (hasTriagePermission) { if (hasTriagePermission) {
console.log('Allowed') console.log('User is allowed. Adding +1 reaction.')
await github.rest.reactions.createForIssueComment({ await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
@ -37,16 +45,18 @@ jobs:
content: '+1', content: '+1',
}) })
} else { } else {
console.log('Not allowed') console.log('User is not allowed. Adding -1 reaction.')
await github.rest.reactions.createForIssueComment({ await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
comment_id: context.payload.comment.id, comment_id: context.payload.comment.id,
content: '-1', 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 id: get-pr-data
with: with:
script: | script: |
@ -59,21 +69,27 @@ jobs:
return { return {
num: context.issue.number, num: context.issue.number,
branchName: pr.head.ref, branchName: pr.head.ref,
commit: pr.head.sha,
repo: pr.head.repo.full_name 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: with:
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' repositories: |
- uses: actions/github-script@v6 svelte
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v7
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
result-encoding: string
script: | script: |
const comment = process.env.COMMENT.trim() const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }} const prData = ${{ steps.get-pr-data.outputs.result }}
@ -89,6 +105,7 @@ jobs:
prNumber: '' + prData.num, prNumber: '' + prData.num,
branchName: prData.branchName, branchName: prData.branchName,
repo: prData.repo, repo: prData.repo,
commit: prData.commit,
suite: suite === '' ? '-' : suite suite: suite === '' ? '-' : suite
} }
}) })

@ -7,6 +7,7 @@ packages/**/config/*.js
# packages/svelte # packages/svelte
packages/svelte/messages/**/*.md packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js
packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js packages/svelte/src/internal/client/errors.js
@ -25,17 +26,7 @@ packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/types packages/svelte/types
packages/svelte/compiler/index.js packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte playgrounds/sandbox/src/*
playgrounds/sandbox/output
# 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 **/node_modules
**/.svelte-kit **/.svelte-kit

@ -17,12 +17,6 @@
"useTabs": false, "useTabs": false,
"tabWidth": 2 "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" "typescript.tsdk": "node_modules/typescript/lib"
} }

@ -101,14 +101,14 @@ Test samples are kept in `/test/xxx/samples` folder.
1. To run test, run `pnpm test`. 1. To run test, run `pnpm test`.
1. To run a particular test suite, use `pnpm test <suite-name>`, for example: 1. To run a particular test suite, use `pnpm test <suite-name>`, for example:
```bash ```sh
pnpm test validator 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 ```sh
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.) (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.)

@ -4,7 +4,7 @@ title: Getting started
We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with: We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with:
```bash ```sh
npx sv create myapp npx sv create myapp
cd myapp cd myapp
npm install npm install

@ -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: 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` ## `$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. ...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 ## 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'). 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). 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` ## `$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. 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.

@ -34,8 +34,10 @@ To mark a style as important, use the `|important` modifier:
<div style:color|important="red">...</div> <div style:color|important="red">...</div>
``` ```
When `style:` directives are combined with `style` attributes, the directives will take precedence: When `style:` directives are combined with `style` attributes, the directives will take precedence,
even over `!important` properties:
```svelte ```svelte
<div style="color: blue;" style:color="red">This will be red</div> <div style:color="red" style="color: blue">This will be red</div>
<div style:color="red" style="color: blue !important">This will still be red</div>
``` ```

@ -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] > [!NOTE]
> This feature was added in 5.3.0 > 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 ## 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` ### `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
<svelte:boundary> <svelte:boundary>

@ -10,7 +10,7 @@ Unit tests allow you to test small isolated parts of your code. Integration test
To setup Vitest manually, first install it: To setup Vitest manually, first install it:
```bash ```sh
npm install -D vitest npm install -D vitest
``` ```
@ -129,12 +129,12 @@ test('Effect', () => {
// effects normally run after a microtask, // effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously // use flushSync to execute all pending effects synchronously
flushSync(); flushSync();
expect(log.value).toEqual([0]); expect(log).toEqual([0]);
count = 1; count = 1;
flushSync(); flushSync();
expect(log.value).toEqual([0, 1]); expect(log).toEqual([0, 1]);
}); });
cleanup(); cleanup();
@ -148,18 +148,14 @@ test('Effect', () => {
*/ */
export function logger(getValue) { export function logger(getValue) {
/** @type {any[]} */ /** @type {any[]} */
let log = $state([]); let log = [];
$effect(() => { $effect(() => {
log.push(getValue()); log.push(getValue());
}); });
return {
get value() {
return log; return log;
} }
};
}
``` ```
### Component testing ### Component testing
@ -170,7 +166,7 @@ It is possible to test your components in isolation using Vitest.
To get started, install jsdom (a library that shims DOM APIs): To get started, install jsdom (a library that shims DOM APIs):
```bash ```sh
npm install -D jsdom npm install -D jsdom
``` ```

@ -83,7 +83,7 @@ If you're using tools like Rollup or Webpack instead, install their respective S
When using TypeScript, make sure your `tsconfig.json` is setup correctly. When using TypeScript, make sure your `tsconfig.json` is setup correctly.
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2022`, or a `target` of at least `ES2015` alongside [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields). This ensures that rune declarations on class fields are not messed with, which would break the Svelte compiler - Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2015` so classes are not compiled to functions
- Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is - Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do. - Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.
@ -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. 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: 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
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:
```ts ```ts
/// file: additional-svelte-typings.d.ts /// file: additional-svelte-typings.d.ts
import { HTMLButtonAttributes } from 'svelte/elements'; import { HTMLButtonAttributes } from 'svelte/elements';
declare module 'svelte/elements' { declare module 'svelte/elements' {
// add a new element
export interface SvelteHTMLElements { export interface SvelteHTMLElements {
'custom-button': HTMLButtonAttributes; '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 { export interface HTMLButtonAttributes {
veryexperimentalattribute?: string; 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 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! --> <!-- 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 ### bind_invalid_checkbox_value
``` ```
@ -68,10 +80,70 @@ 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) `%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 ### 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
```
`getAbortSignal()` can only be called inside an effect or derived
``` ```
### hydration_failed ### hydration_failed
@ -110,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 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 ### state_descriptors_fixed
``` ```
@ -131,7 +211,7 @@ Cannot set prototype of `$state` object
### state_unsafe_mutation ### state_unsafe_mutation
``` ```
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
``` ```
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
@ -164,3 +244,23 @@ let odd = $derived(!even);
``` ```
If side-effects are unavoidable, use [`$effect`]($effect) instead. 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 ### 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. 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 ### transition_slide_display
``` ```

@ -364,6 +364,12 @@ The $ name is reserved, and cannot be used for variables and imports
The $ prefix is reserved, and cannot be used for variables and imports The $ prefix is reserved, and cannot be used for variables and imports
``` ```
### duplicate_class_field
```
`%name%` has already been declared
```
### each_item_invalid_assignment ### each_item_invalid_assignment
``` ```
@ -480,6 +486,12 @@ Expected token %token%
Expected whitespace 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 ### export_undefined
``` ```
@ -534,6 +546,12 @@ The arguments keyword cannot be used within the template or at the top level of
%message% %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 ### legacy_export_invalid
``` ```

@ -683,7 +683,7 @@ Some templating languages (including Svelte) will 'fix' HTML by turning `<span /
To automate this, run the dedicated migration: To automate this, run the dedicated migration:
```bash ```sh
npx sv migrate self-closing-tags npx sv migrate self-closing-tags
``` ```

@ -1,5 +1,25 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! --> <!-- 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 ### invalid_default_snippet
``` ```

@ -2,4 +2,6 @@
title: svelte/action title: svelte/action
--- ---
This module provides types for [actions](use), which have been superseded by [attachments](@attach).
> MODULE: svelte/action > MODULE: svelte/action

@ -49,12 +49,13 @@ export default [
}, },
rules: { rules: {
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/require-await': 'error', '@typescript-eslint/require-await': 'error',
'no-console': 'error', 'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }], '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 // 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', 'object-shorthand': 'off',
// eslint is being a dummy here too
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'no-var': 'off', 'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix` // TODO: enable these rules and run `pnpm lint:fix`
@ -79,7 +80,8 @@ export default [
files: ['packages/svelte/src/**/*'], files: ['packages/svelte/src/**/*'],
ignores: ['packages/svelte/src/compiler/**/*'], ignores: ['packages/svelte/src/compiler/**/*'],
rules: { rules: {
'custom/no_compiler_imports': 'error' 'custom/no_compiler_imports': 'error',
'svelte/no-svelte-internal': 'off'
} }
}, },
{ {
@ -87,6 +89,7 @@ export default [
'**/*.d.ts', '**/*.d.ts',
'**/tests', '**/tests',
'packages/svelte/scripts/process-messages/templates/*.js', 'packages/svelte/scripts/process-messages/templates/*.js',
'packages/svelte/scripts/_bundle.js',
'packages/svelte/src/compiler/errors.js', 'packages/svelte/src/compiler/errors.js',
'packages/svelte/src/internal/client/errors.js', 'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js', 'packages/svelte/src/internal/client/warnings.js',
@ -100,11 +103,7 @@ export default [
'*.config.js', '*.config.js',
// documentation can contain invalid examples // documentation can contain invalid examples
'documentation', 'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules 'tmp/**'
'sites/svelte-5-preview/**',
'tmp/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
] ]
} }
]; ];

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

@ -1,5 +1,323 @@
# svelte # svelte
## 5.37.2
### Patch Changes
- fix: double event processing in firefox due to event object being garbage collected ([#16527](https://github.com/sveltejs/svelte/pull/16527))
- fix: add bindable dimension attributes types to SVG and MathML elements ([#16525](https://github.com/sveltejs/svelte/pull/16525))
- fix: correctly differentiate static fields before emitting `duplicate_class_field` ([#16526](https://github.com/sveltejs/svelte/pull/16526))
- fix: prevent last_propagated_event from being DCE'd ([#16538](https://github.com/sveltejs/svelte/pull/16538))
## 5.37.1
### Patch Changes
- chore: remove some todos ([#16515](https://github.com/sveltejs/svelte/pull/16515))
- fix: allow await expressions inside `{#await ...}` argument ([#16514](https://github.com/sveltejs/svelte/pull/16514))
- fix: `append_styles` in an effect to make them available on mount ([#16509](https://github.com/sveltejs/svelte/pull/16509))
- chore: remove `parser.template_untrimmed` ([#16511](https://github.com/sveltejs/svelte/pull/16511))
- fix: always inject styles when compiling as a custom element ([#16509](https://github.com/sveltejs/svelte/pull/16509))
## 5.37.0
### Minor Changes
- feat: ignore component options in `compileModule` ([#16362](https://github.com/sveltejs/svelte/pull/16362))
### Patch Changes
- fix: always mark props as stateful ([#16504](https://github.com/sveltejs/svelte/pull/16504))
## 5.36.17
### Patch Changes
- fix: throw on duplicate class field declarations ([#16502](https://github.com/sveltejs/svelte/pull/16502))
- fix: add types for `part` attribute to svg attributes ([#16499](https://github.com/sveltejs/svelte/pull/16499))
## 5.36.16
### Patch Changes
- fix: don't update a focused input with values from its own past ([#16491](https://github.com/sveltejs/svelte/pull/16491))
- fix: don't destroy effect roots created inside of deriveds ([#16492](https://github.com/sveltejs/svelte/pull/16492))
## 5.36.15
### Patch Changes
- fix: preserve dirty status of deferred effects ([#16487](https://github.com/sveltejs/svelte/pull/16487))
## 5.36.14
### Patch Changes
- fix: keep input in sync when binding updated via effect ([#16482](https://github.com/sveltejs/svelte/pull/16482))
- fix: rename form accept-charset attribute ([#16478](https://github.com/sveltejs/svelte/pull/16478))
- fix: prevent infinite async loop ([#16482](https://github.com/sveltejs/svelte/pull/16482))
- fix: exclude derived writes from effect abort and rescheduling ([#16482](https://github.com/sveltejs/svelte/pull/16482))
## 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
- fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction ([#16325](https://github.com/sveltejs/svelte/pull/16325))
- fix: re-evaluate derived props during teardown ([#16278](https://github.com/sveltejs/svelte/pull/16278))
## 5.35.4
### Patch Changes
- fix: abort and reschedule effect processing after state change in user effect ([#16280](https://github.com/sveltejs/svelte/pull/16280))
## 5.35.3
### Patch Changes
- fix: account for mounting when `select_option` in `attribute_effect` ([#16309](https://github.com/sveltejs/svelte/pull/16309))
- fix: do not proxify the value assigned to a derived ([#16302](https://github.com/sveltejs/svelte/pull/16302))
## 5.35.2
### Patch Changes
- fix: bump esrap ([#16295](https://github.com/sveltejs/svelte/pull/16295))
## 5.35.1
### Patch Changes
- feat: add parent hierarchy to `__svelte_meta` objects ([#16255](https://github.com/sveltejs/svelte/pull/16255))
## 5.35.0
### Minor Changes
- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266))
### Patch Changes
- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270))
## 5.34.9
### Patch Changes
- fix: ensure unowned deriveds can add themselves as reactions while connected ([#16249](https://github.com/sveltejs/svelte/pull/16249))
## 5.34.8
### Patch Changes
- fix: untrack `$inspect.with` and add check for unsafe mutation ([#16209](https://github.com/sveltejs/svelte/pull/16209))
- fix: use fine grained for template if the component is not explicitly in legacy mode ([#16232](https://github.com/sveltejs/svelte/pull/16232))
- lift unsafe_state_mutation constraints for SvelteSet, SvelteMap, SvelteDate, SvelteURL and SvelteURLSearchParams created inside the derived ([#16221](https://github.com/sveltejs/svelte/pull/16221))
## 5.34.7
### Patch Changes
- fix: address css class matching regression ([#16204](https://github.com/sveltejs/svelte/pull/16204))
## 5.34.6
### Patch Changes
- fix: match class and style directives against attribute selector ([#16179](https://github.com/sveltejs/svelte/pull/16179))
## 5.34.5
### Patch Changes
- fix: keep spread non-delegated event handlers up to date ([#16180](https://github.com/sveltejs/svelte/pull/16180))
- fix: remove undefined attributes on hydration ([#16178](https://github.com/sveltejs/svelte/pull/16178))
- fix: ensure sources within nested effects still register correctly ([#16193](https://github.com/sveltejs/svelte/pull/16193))
- fix: avoid shadowing a variable in dynamic components ([#16185](https://github.com/sveltejs/svelte/pull/16185))
## 5.34.4
### Patch Changes
- fix: don't set state withing `with_parent` in proxy ([#16176](https://github.com/sveltejs/svelte/pull/16176))
- fix: use compiler-driven reactivity in legacy mode template expressions ([#16100](https://github.com/sveltejs/svelte/pull/16100))
## 5.34.3
### Patch Changes
- fix: don't eagerly execute deriveds on resume ([#16150](https://github.com/sveltejs/svelte/pull/16150))
- fix: prevent memory leaking signals in legacy mode ([#16145](https://github.com/sveltejs/svelte/pull/16145))
- fix: don't define `error.message` if it's not configurable ([#16149](https://github.com/sveltejs/svelte/pull/16149))
## 5.34.2
### Patch Changes
- fix: add missing typings for some dimension bindings ([#16142](https://github.com/sveltejs/svelte/pull/16142))
- fix: prune typescript class field declarations ([#16154](https://github.com/sveltejs/svelte/pull/16154))
## 5.34.1 ## 5.34.1
### Patch Changes ### Patch Changes

@ -19,7 +19,7 @@ You can play around with Svelte in the [tutorial](https://svelte.dev/tutorial),
When you're ready to build a full-fledge application, we recommend using [SvelteKit](https://svelte.dev/docs/kit): When you're ready to build a full-fledge application, we recommend using [SvelteKit](https://svelte.dev/docs/kit):
```bash ```sh
npx sv create my-app npx sv create my-app
cd my-app cd my-app
npm install npm install

@ -464,6 +464,14 @@ export interface DOMAttributes<T extends EventTarget> {
onfullscreenerror?: EventHandler<Event, T> | undefined | null; onfullscreenerror?: EventHandler<Event, T> | undefined | null;
onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null; onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null;
// Dimensions
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:clientWidth'?: number | undefined | null;
readonly 'bind:clientHeight'?: number | undefined | null;
xmlns?: string | undefined | null; xmlns?: string | undefined | null;
} }
@ -839,11 +847,9 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
*/ */
'bind:innerText'?: string | undefined | null; 'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null; readonly 'bind:focused'?: boolean | undefined | null;
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;
// SvelteKit // SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@ -992,7 +998,7 @@ export interface HTMLFieldsetAttributes extends HTMLAttributes<HTMLFieldSetEleme
} }
export interface HTMLFormAttributes extends HTMLAttributes<HTMLFormElement> { export interface HTMLFormAttributes extends HTMLAttributes<HTMLFormElement> {
acceptcharset?: string | undefined | null; 'accept-charset'?: 'utf-8' | (string & {}) | undefined | null;
action?: string | undefined | null; action?: string | undefined | null;
autocomplete?: AutoFillBase | undefined | null; autocomplete?: AutoFillBase | undefined | null;
enctype?: enctype?:
@ -1549,6 +1555,7 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
height?: number | string | undefined | null; height?: number | string | undefined | null;
id?: string | undefined | null; id?: string | undefined | null;
lang?: string | undefined | null; lang?: string | undefined | null;
part?: string | undefined | null;
max?: number | string | undefined | null; max?: number | string | undefined | null;
media?: string | undefined | null; media?: string | undefined | null;
// On the `textPath` element // On the `textPath` element
@ -2076,6 +2083,7 @@ export interface SvelteHTMLElements {
'svelte:boundary': { 'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void; onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>; failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
pending?: import('svelte').Snippet;
}; };
[name: string]: { [name: string]: any }; [name: string]: { [name: string]: any };

@ -1,10 +1,6 @@
{ {
"$schema": "https://unpkg.com/knip@5/schema.json", "$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [ "entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js", "tests/**/*.js",
"tests/**/*.ts", "tests/**/*.ts",
"!tests/**/*.svelte", "!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 ## bind_invalid_checkbox_value
> Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead > Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
@ -44,9 +54,63 @@ 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) > `%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 ## 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
> `getAbortSignal()` can only be called inside an effect or derived
## hydration_failed ## hydration_failed
@ -72,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 > 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 ## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. > Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
@ -86,7 +156,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
## state_unsafe_mutation ## state_unsafe_mutation
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` > Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
@ -118,3 +188,21 @@ let odd = $derived(!even);
``` ```
If side-effects are unavoidable, use [`$effect`]($effect) instead. 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_property_non_reactive
> `%binding%` is binding to a non-reactive property > `%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. 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 ## transition_slide_display
> The `slide` transition does not work correctly for elements with `display: %value%` > The `slide` transition does not work correctly for elements with `display: %value%`

@ -30,6 +30,10 @@
> The $ prefix is reserved, and cannot be used for variables and imports > The $ prefix is reserved, and cannot be used for variables and imports
## duplicate_class_field
> `%name%` has already been declared
## each_item_invalid_assignment ## each_item_invalid_assignment
> Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
@ -70,6 +74,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `$effect()` can only be used as an expression statement > `$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 ## export_undefined
> `%name%` is not defined > `%name%` is not defined
@ -98,6 +106,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 > 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 ## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead > Cannot use `export let` in runes mode — use `$props()` instead

@ -71,7 +71,7 @@ Some templating languages (including Svelte) will 'fix' HTML by turning `<span /
To automate this, run the dedicated migration: To automate this, run the dedicated migration:
```bash ```sh
npx sv migrate self-closing-tags npx sv migrate self-closing-tags
``` ```

@ -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 ## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead > Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.34.1", "version": "5.37.2",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -59,6 +59,9 @@
"./internal/disclose-version": { "./internal/disclose-version": {
"default": "./src/internal/disclose-version.js" "default": "./src/internal/disclose-version.js"
}, },
"./internal/flags/async": {
"default": "./src/internal/flags/async.js"
},
"./internal/flags/legacy": { "./internal/flags/legacy": {
"default": "./src/internal/flags/legacy.js" "default": "./src/internal/flags/legacy.js"
}, },
@ -164,14 +167,14 @@
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"acorn": "^8.12.1", "acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^1.4.8", "esrap": "^2.1.0",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",

@ -118,36 +118,40 @@ const bundle = await bundle_code(
).js.code ).js.code
); );
if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) { /**
* @param {string} case_name
* @param {string[]} strings
*/
function check_bundle(case_name, ...strings) {
for (const string of strings) {
const index = bundle.indexOf(string);
if (index >= 0) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`); console.error(`${case_name} not treeshakeable`);
} else {
failed = true; failed = true;
// eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`);
}
if (!bundle.includes('component_context.l')) { let lines = bundle.slice(index - 500, index + 500).split('\n');
// eslint-disable-next-line no-console const target_line = lines.findIndex((line) => line.includes(string));
console.error(`✅ Legacy code treeshakeable`); // mark the failed line
} else { lines = lines
failed = true; .map((line, i) => (i === target_line ? `> ${line}` : `| ${line}`))
.slice(target_line - 5, target_line + 6);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`❌ Legacy code not treeshakeable`); console.error('The first failed line:\n' + lines.join('\n'));
return;
}
} }
if (!bundle.includes(`'CreatedAt'`)) {
// eslint-disable-next-line no-console
console.error(`$inspect.trace code treeshakeable`);
} else {
failed = true;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`$inspect.trace code not treeshakeable`); console.error(`${case_name} treeshakeable`);
} }
check_bundle('Hydration code', 'hydrate_node', 'hydrate_next');
check_bundle('Legacy code', 'component_context.l');
check_bundle('$inspect.trace', `'CreatedAt'`);
if (failed) { if (failed) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(bundle); console.error('Full bundle at', path.resolve('scripts/_bundle.js'));
fs.writeFileSync('scripts/_bundle.js', bundle); fs.writeFileSync('scripts/_bundle.js', bundle);
} }

@ -1,9 +1,14 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import * as ESTree from 'estree' */
/** @import { AST } from 'svelte/compiler' */
// @ts-check // @ts-check
import process from 'node:process'; import process from 'node:process';
import fs from 'node:fs'; import fs from 'node:fs';
import * as acorn from 'acorn'; import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as esrap from 'esrap'; import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';
const DIR = '../../documentation/docs/98-reference/.generated'; const DIR = '../../documentation/docs/98-reference/.generated';
@ -97,79 +102,49 @@ function run() {
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8') .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n'); .replace(/\r\n/g, '\n');
/** /** @type {AST.JSComment[]} */
* @type {Array<{
* type: string;
* value: string;
* start: number;
* end: number
* }>}
*/
const comments = []; const comments = [];
let ast = acorn.parse(source, { let ast = /** @type {ESTree.Node} */ (
/** @type {unknown} */ (
acorn.parse(source, {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
onComment: (block, value, start, end) => { locations: true,
if (block && /\n/.test(value)) { onComment: comments
let a = start; })
while (a > 0 && source[a - 1] !== '\n') a -= 1; )
);
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end }); comments.forEach((comment) => {
if (comment.type === 'Block') {
comment.value = comment.value.replace(/^\t+/gm, '');
} }
}); });
ast = walk(ast, null, { ast = walk(ast, null, {
_(node, { next }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);
if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
},
// @ts-expect-error
Identifier(node, context) { Identifier(node, context) {
if (node.name === 'CODES') { if (node.name === 'CODES') {
return { /** @type {ESTree.ArrayExpression} */
const array = {
type: 'ArrayExpression', type: 'ArrayExpression',
elements: Object.keys(messages[name]).map((code) => ({ elements: Object.keys(messages[name]).map((code) => ({
type: 'Literal', type: 'Literal',
value: code value: code
})) }))
}; };
return array;
} }
} }
}); });
if (comments.length > 0) { const body = /** @type {ESTree.Program} */ (ast).body;
// @ts-expect-error
(ast.trailingComments ||= []).push(...comments);
}
const category = messages[name]; const category = messages[name];
// find the `export function CODE` node // find the `export function CODE` node
const index = ast.body.findIndex((node) => { const index = body.findIndex((node) => {
if ( if (
node.type === 'ExportNamedDeclaration' && node.type === 'ExportNamedDeclaration' &&
node.declaration && node.declaration &&
@ -181,8 +156,19 @@ function run() {
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`); if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
const template_node = ast.body[index]; const template_node = body[index];
ast.body.splice(index, 1); body.splice(index, 1);
const jsdoc = /** @type {AST.JSComment} */ (
comments.findLast((comment) => comment.start < /** @type {number} */ (template_node.start))
);
const printed = esrap.print(
/** @type {Node} */ (ast),
ts({
comments: comments.filter((comment) => comment !== jsdoc)
})
);
for (const code in category) { for (const code in category) {
const { messages } = category[code]; const { messages } = category[code];
@ -203,7 +189,7 @@ function run() {
}; };
}); });
/** @type {import('estree').Expression} */ /** @type {ESTree.Expression} */
let message = { type: 'Literal', value: '' }; let message = { type: 'Literal', value: '' };
let prev_vars; let prev_vars;
@ -221,10 +207,10 @@ function run() {
const parts = text.split(/(%\w+%)/); const parts = text.split(/(%\w+%)/);
/** @type {import('estree').Expression[]} */ /** @type {ESTree.Expression[]} */
const expressions = []; const expressions = [];
/** @type {import('estree').TemplateElement[]} */ /** @type {ESTree.TemplateElement[]} */
const quasis = []; const quasis = [];
for (let i = 0; i < parts.length; i += 1) { for (let i = 0; i < parts.length; i += 1) {
@ -244,7 +230,7 @@ function run() {
} }
} }
/** @type {import('estree').Expression} */ /** @type {ESTree.Expression} */
const expression = { const expression = {
type: 'TemplateLiteral', type: 'TemplateLiteral',
expressions, expressions,
@ -272,42 +258,8 @@ function run() {
prev_vars = vars; prev_vars = vars;
} }
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, { const clone = /** @type {ESTree.Statement} */ (
// @ts-expect-error Block is a block comment, which is not recognised walk(/** @type {ESTree.Node} */ (template_node), null, {
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;
const value = /** @type {string} */ (node.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
return line;
})
.filter((x) => x !== '')
.join('\n');
if (value !== node.value) {
return { ...node, value };
}
},
FunctionDeclaration(node, context) { FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return; if (node.id.name !== 'CODE') return;
@ -321,8 +273,8 @@ function run() {
} }
} }
return /** @type {import('estree').FunctionDeclaration} */ ({ return /** @type {ESTree.FunctionDeclaration} */ ({
.../** @type {import('estree').FunctionDeclaration} */ (context.next()), .../** @type {ESTree.FunctionDeclaration} */ (context.next()),
params, params,
id: { id: {
...node.id, ...node.id,
@ -331,7 +283,7 @@ function run() {
}); });
}, },
TemplateLiteral(node, context) { TemplateLiteral(node, context) {
/** @type {import('estree').TemplateElement} */ /** @type {ESTree.TemplateElement} */
let quasi = { let quasi = {
type: 'TemplateElement', type: 'TemplateElement',
value: { value: {
@ -340,7 +292,7 @@ function run() {
tail: node.quasis[0].tail tail: node.quasis[0].tail
}; };
/** @type {import('estree').TemplateLiteral} */ /** @type {ESTree.TemplateLiteral} */
let out = { let out = {
type: 'TemplateLiteral', type: 'TemplateLiteral',
quasis: [quasi], quasis: [quasi],
@ -375,7 +327,7 @@ function run() {
} }
out.quasis.push((quasi = q)); out.quasis.push((quasi = q));
out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e))); out.expressions.push(/** @type {ESTree.Expression} */ (context.visit(e)));
} }
return out; return out;
@ -392,18 +344,54 @@ function run() {
if (node.name !== 'MESSAGE') return; if (node.name !== 'MESSAGE') return;
return message; return message;
} }
}); })
);
// @ts-expect-error const jsdoc_clone = {
ast.body.push(clone); ...jsdoc,
value: /** @type {string} */ (jsdoc.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
} }
const module = esrap.print(ast); if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
return line;
})
.filter((x) => x !== '')
.join('\n')
};
const block = esrap.print(
// @ts-expect-error some bullshit
/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
ts({ comments: [jsdoc_clone] })
).code;
printed.code += `\n\n${block}`;
body.push(clone);
}
fs.writeFileSync( fs.writeFileSync(
dest, dest,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` + `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code, printed.code,
'utf-8' 'utf-8'
); );
} }

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

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

@ -281,6 +281,13 @@ declare namespace $effect {
*/ */
export function pre(fn: () => void | (() => void)): void; 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. * 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.
* *

@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) { constructor(code, message, position) {
super(message); super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable
// We want to extend from Error so that various bundler plugins properly handle it. // We want to extend from Error so that various bundler plugins properly handle it.
// But we also want to share the same object shape with that of warnings, therefore // But we also want to share the same object shape with that of warnings, therefore
// we create an instance of the shared class an copy over its properties. // we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position); this.#diagnostic = new CompileDiagnostic(code, message, position);
Object.assign(this, this.#diagnostic); Object.assign(this, this.#diagnostic);
this.name = 'CompileError'; this.name = 'CompileError';
} }
@ -150,6 +152,16 @@ export function dollar_prefix_invalid(node) {
e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`); e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`);
} }
/**
* `%name%` has already been declared
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function duplicate_class_field(node, name) {
e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`);
}
/** /**
* Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -168,6 +180,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`); 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 * `%name%` is not defined
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -233,6 +254,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`); 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 * Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -834,7 +864,9 @@ export function bind_invalid_expression(node) {
* @returns {never} * @returns {never}
*/ */
export function bind_invalid_name(node, name, explanation) { export function bind_invalid_name(node, name, explanation) {
e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`); e(node, 'bind_invalid_name', `${explanation
? `\`bind:${name}\` is not a valid binding. ${explanation}`
: `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
} }
/** /**

@ -3,7 +3,6 @@
/** @import { AST } from './public.js' */ /** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe'; import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js'; import { convert } from './legacy.js';
import { parse as parse_acorn } from './phases/1-parse/acorn.js';
import { parse as _parse } from './phases/1-parse/index.js'; import { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js'; import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js'; import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
@ -21,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js';
*/ */
export function compile(source, options) { export function compile(source, options) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(options.warningFilter); state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source); let parsed = _parse(source);
@ -65,11 +63,10 @@ export function compile(source, options) {
*/ */
export function compileModule(source, options) { export function compileModule(source, options) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(options.warningFilter); state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_module_options(options, ''); const validated = validate_module_options(options, '');
state.reset(source, validated);
const analysis = analyze_module(parse_acorn(source, false), validated); const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated); return transform_module(analysis, source, validated);
} }
@ -97,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record<string, any>} * @returns {Record<string, any>}
*/ */
// TODO 6.0 remove unused `filename`
/** /**
* The parse function parses a component, returning only its abstract syntax tree. * The parse function parses a component, returning only its abstract syntax tree.
* *
@ -105,14 +103,15 @@ export function compileModule(source, options) {
* *
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile. * The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
* *
* The `filename` option is unused and will be removed in Svelte 6.0.
*
* @param {string} source * @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options] * @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot} * @returns {AST.Root | LegacyRoot}
*/ */
export function parse(source, { filename, rootDir, modern, loose } = {}) { export function parse(source, { modern, loose } = {}) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(() => false); state.reset({ warning: () => false, filename: undefined });
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source, loose); const ast = _parse(source, loose);
return to_public_ast(source, ast, modern); return to_public_ast(source, ast, modern);

@ -451,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) { SpreadAttribute(node) {
return { ...node, type: 'Spread' }; return { ...node, type: 'Spread' };
}, },
// @ts-ignore
StyleSheet(node, context) { StyleSheet(node, context) {
return { return {
...node, ...node,

@ -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 { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js'; import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js'; import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js'; import { reset, UNKNOWN_FILENAME } from '../state.js';
import { import {
extract_identifiers, extract_identifiers,
extract_all_identifiers_from_expression, extract_all_identifiers_from_expression,
@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end; return start + style_placeholder + end;
}); });
reset_warning_filter(() => false); reset({ warning: () => false, filename });
reset(source, { filename: filename ?? '(unknown)' });
let parsed = parse(source); let parsed = parse(source);
@ -146,7 +145,10 @@ export function migrate(source, { filename, use_ts } = {}) {
...validate_component_options({}, ''), ...validate_component_options({}, ''),
...parsed_options, ...parsed_options,
customElementOptions, customElementOptions,
filename: filename ?? '(unknown)' filename: filename ?? UNKNOWN_FILENAME,
experimental: {
async: true
}
}; };
const str = new MagicString(source); const str = new MagicString(source);
@ -1705,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) {
} }
// Ensure modifiers are applied in the same order as Svelte 4 // Ensure modifiers are applied in the same order as Svelte 4
const modifier_order = [ const modifier_order = /** @type {const} */ ([
'preventDefault', 'preventDefault',
'stopPropagation', 'stopPropagation',
'stopImmediatePropagation', 'stopImmediatePropagation',
'self', 'self',
'trusted', 'trusted',
'once' 'once'
]; ]);
/** /**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element

@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */ /** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn'; import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript'; import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin()); const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** /**
* @param {string} source * @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {boolean} [is_script] * @param {boolean} [is_script]
*/ */
export function parse(source, typescript, is_script) { export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser; const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore // @ts-ignore
const parse_statement = parser.prototype.parseStatement; const parse_statement = parser.prototype.parseStatement;
@ -53,13 +67,19 @@ export function parse(source, typescript, is_script) {
/** /**
* @param {string} source * @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {number} index * @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} * @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/ */
export function parse_expression_at(source, typescript, index) { export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser; const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const ast = parser.parseExpressionAt(source, index, { const ast = parser.parseExpressionAt(source, index, {
onComment, onComment,
@ -78,26 +98,20 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments * to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting. * in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source * @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/ */
function get_comment_handlers(source) { function get_comment_handlers(source, comments, index = 0) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return { return {
/** /**
* @param {boolean} block * @param {boolean} block
* @param {string} value * @param {string} value
* @param {number} start * @param {number} start
* @param {number} end * @param {number} end
* @param {import('acorn').Position} [start_loc]
* @param {import('acorn').Position} [end_loc]
*/ */
onComment: (block, value, start, end) => { onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) { if (block && /\n/.test(value)) {
let a = start; let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1; while (a > 0 && source[a - 1] !== '\n') a -= 1;
@ -109,13 +123,26 @@ function get_comment_handlers(source) {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
} }
comments.push({ type: block ? 'Block' : 'Line', value, start, end }); comments.push({
type: block ? 'Block' : 'Line',
value,
start,
end,
loc: {
start: /** @type {import('acorn').Position} */ (start_loc),
end: /** @type {import('acorn').Position} */ (end_loc)
}
});
}, },
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */ /** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) { add_comments(ast) {
if (comments.length === 0) return; if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, { walk(ast, null, {
_(node, { next, path }) { _(node, { next, path }) {
let comment; let comment;

@ -8,6 +8,7 @@ import { create_fragment } from './utils/create.js';
import read_options from './read/options.js'; import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js'; import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
@ -21,12 +22,6 @@ export class Parser {
*/ */
template; template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/** /**
* Whether or not we're in loose parsing mode, in which * Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible * case we try to continue parsing as much as possible
@ -65,7 +60,6 @@ export class Parser {
} }
this.loose = loose; this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd(); this.template = template.trimEnd();
let match_lang; let match_lang;
@ -87,6 +81,7 @@ export class Parser {
type: 'Root', type: 'Root',
fragment: create_fragment(), fragment: create_fragment(),
options: null, options: null,
comments: [],
metadata: { metadata: {
ts: this.ts ts: this.ts
} }
@ -299,6 +294,8 @@ export class Parser {
* @returns {AST.Root} * @returns {AST.Root}
*/ */
export function parse(template, loose = false) { export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose); const parser = new Parser(template, loose);
return parser.root; return parser.root;
} }

@ -59,7 +59,12 @@ export default function read_pattern(parser) {
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ ( const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left; ).left;
expression.typeAnnotation = read_type_annotation(parser); expression.typeAnnotation = read_type_annotation(parser);
@ -96,13 +101,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional // parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error. // parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a); let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it // `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') { if (expression.type === 'AssignmentExpression') {
let b = expression.right.start; let b = expression.right.start;
while (template[b] !== '=') b -= 1; while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a); expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
} }
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that // `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) {
*/ */
export default function read_expression(parser, opening_token, disallow_loose) { export default function read_expression(parser, opening_token, disallow_loose) {
try { try {
const node = parse_expression_at(parser.template, parser.ts, parser.index); let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0; let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) { let i = parser.root.comments.length;
parser.index = node.leadingComments.at(-1).end; while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
} }
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) { for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) {
} }
let index = /** @type {number} */ (node.end); let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end; const last_comment = parser.root.comments.at(-1);
} if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) { while (num_parens > 0) {
const char = parser.template[index]; const char = parser.template[index];

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast; let ast;
try { try {
ast = acorn.parse(source, parser.ts, true); ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) { } catch (err) {
parser.acorn_error(err); parser.acorn_error(err);
} }

@ -115,6 +115,19 @@ const visitors = {
TSDeclareFunction() { TSDeclareFunction() {
return b.empty; return b.empty;
}, },
ClassBody(node, context) {
const body = [];
for (const _child of node.body) {
const child = context.visit(_child);
if (child.type !== 'PropertyDefinition' || !child.declare) {
body.push(child);
}
}
return {
...node,
body
};
},
ClassDeclaration(node, context) { ClassDeclaration(node, context) {
if (node.declare) { if (node.declare) {
return b.empty; return b.empty;

@ -295,6 +295,8 @@ export default function element(parser) {
} else { } else {
element.tag = get_attribute_expression(definition); element.tag = get_attribute_expression(definition);
} }
element.metadata.expression = create_expression_metadata();
} }
if (is_top_level_script_or_style) { if (is_top_level_script_or_style) {
@ -368,14 +370,6 @@ export default function element(parser) {
// ... or we're followed by whitespace, for example near the end of the template, // ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with // which we want to take in so that language tools has more room to work with
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
} }
} }
} }

@ -63,7 +63,10 @@ function open(parser) {
end: -1, end: -1,
test: read_expression(parser), test: read_expression(parser),
consequent: create_fragment(), consequent: create_fragment(),
alternate: null alternate: null,
metadata: {
expression: create_expression_metadata()
}
}); });
parser.allow_whitespace(); parser.allow_whitespace();
@ -244,7 +247,10 @@ function open(parser) {
error: null, error: null,
pending: null, pending: null,
then: null, then: null,
catch: null catch: null,
metadata: {
expression: create_expression_metadata()
}
}); });
if (parser.eat('then')) { if (parser.eat('then')) {
@ -326,7 +332,10 @@ function open(parser) {
start, start,
end: -1, end: -1,
expression, expression,
fragment: create_fragment() fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
}
}); });
parser.stack.push(block); parser.stack.push(block);
@ -389,7 +398,12 @@ function open(parser) {
let function_expression = matched let function_expression = matched
? /** @type {ArrowFunctionExpression} */ ( ? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start) parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
) )
: { params: [] }; : { params: [] };
@ -461,7 +475,10 @@ function next(parser) {
elseif: true, elseif: true,
test: expression, test: expression,
consequent: create_fragment(), consequent: create_fragment(),
alternate: null alternate: null,
metadata: {
expression: create_expression_metadata()
}
}); });
parser.stack.push(child); parser.stack.push(child);
@ -624,7 +641,10 @@ function special(parser) {
type: 'HtmlTag', type: 'HtmlTag',
start, start,
end: parser.index, end: parser.index,
expression expression,
metadata: {
expression: create_expression_metadata()
}
}); });
return; return;
@ -699,6 +719,9 @@ function special(parser) {
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const start: start + 2, // start at const, not at @const
end: parser.index - 1 end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
} }
}); });
} }
@ -725,6 +748,7 @@ function special(parser) {
end: parser.index, end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression), expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: { metadata: {
expression: create_expression_metadata(),
dynamic: false, dynamic: false,
arguments: [], arguments: [],
path: [], path: [],

@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} }
case 'ClassSelector': { case 'ClassSelector': {
if ( if (!attribute_matches(element, 'class', name, '~=', false)) {
!attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
return false; return false;
} }
@ -633,14 +628,33 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
if (attribute.type === 'SpreadAttribute') return true; if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true; if (attribute.type === 'BindDirective' && attribute.name === name) return true;
const name_lower = name.toLowerCase();
// match attributes against the corresponding directive but bail out on exact matching
if (attribute.type === 'StyleDirective' && name_lower === 'style') return true;
if (attribute.type === 'ClassDirective' && name_lower === 'class') {
if (operator === '~=') {
if (attribute.name === expected_value) return true;
} else {
return true;
}
}
if (attribute.type !== 'Attribute') continue; if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; if (attribute.name.toLowerCase() !== name_lower) continue;
if (attribute.value === true) return operator === null; if (attribute.value === true) return operator === null;
if (expected_value === null) return true; if (expected_value === null) return true;
if (is_text_attribute(attribute)) { if (is_text_attribute(attribute)) {
return test_attribute(operator, expected_value, case_insensitive, attribute.value[0].data); const matches = test_attribute(
operator,
expected_value,
case_insensitive,
attribute.value[0].data
);
// continue if we still may match against a class/style directive
if (!matches && (name_lower === 'class' || name_lower === 'style')) continue;
return matches;
} }
const chunks = get_attribute_chunks(attribute.value); const chunks = get_attribute_chunks(attribute.value);
@ -649,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
/** @type {string[]} */ /** @type {string[]} */
let prev_values = []; let prev_values = [];
for (const chunk of chunks) { for (const chunk of chunks) {
const current_possible_values = get_possible_values(chunk, name === 'class'); const current_possible_values = get_possible_values(chunk, name_lower === 'class');
// impossible to find out all combinations // impossible to find out all combinations
if (!current_possible_values) return true; if (!current_possible_values) return true;

@ -3,6 +3,7 @@
/** @import { AnalysisState, Visitors } from './types' */ /** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js'; import { extract_identifiers } from '../../utils/ast.js';
@ -21,6 +22,7 @@ import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AttachTag } from './visitors/AttachTag.js'; import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js'; import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BindDirective } from './visitors/BindDirective.js'; import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js'; import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js'; import { ClassBody } from './visitors/ClassBody.js';
@ -75,6 +77,7 @@ import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
import * as state from '../../state.js';
/** /**
* @type {Visitors} * @type {Visitors}
@ -138,6 +141,7 @@ const visitors = {
AttachTag, AttachTag,
Attribute, Attribute,
AwaitBlock, AwaitBlock,
AwaitExpression,
BindDirective, BindDirective,
CallExpression, CallExpression,
ClassBody, ClassBody,
@ -209,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) {
body: [] 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 };
} }
/** /**
@ -231,12 +240,18 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots']; const RESERVED = ['$$props', '$$restProps', '$$slots'];
/** /**
* @param {Program} ast * @param {string} source
* @param {ValidatedModuleCompileOptions} options * @param {ValidatedModuleCompileOptions} options
* @returns {Analysis} * @returns {Analysis}
*/ */
export function analyze_module(ast, options) { export function analyze_module(source, options) {
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); /** @type {AST.JSComment[]} */
const comments = [];
state.set_source(source);
const ast = parse(source, comments, false, false);
const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) { for (const [name, references] of scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue; if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -253,15 +268,23 @@ export function analyze_module(ast, options) {
/** @type {Analysis} */ /** @type {Analysis} */
const analysis = { const analysis = {
module: { ast, scope, scopes }, module: { ast, scope, scopes, has_await },
name: options.filename, name: options.filename,
accessors: false, accessors: false,
runes: true, runes: true,
immutable: true, immutable: true,
tracing: false, tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map() classes: new Map()
}; };
state.adjust({
dev: options.dev,
rootDir: options.rootDir,
runes: true
});
walk( walk(
/** @type {Node} */ (ast), /** @type {Node} */ (ast),
{ {
@ -298,7 +321,12 @@ export function analyze_component(root, source, options) {
const module = js(root.module, scope_root, false, null); const module = js(root.module, scope_root, false, null);
const instance = js(root.instance, scope_root, true, module.scope); 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} */ /** @type {Template} */
const template = { ast: root.fragment, scope, scopes }; const template = { ast: root.fragment, scope, scopes };
@ -406,7 +434,9 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename); 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) { if (!runes) {
for (let check of synthetic_stores_legacy_check) { for (let check of synthetic_stores_legacy_check) {
@ -421,6 +451,8 @@ export function analyze_component(root, source, options) {
} }
} }
const is_custom_element = !!options.customElementOptions || options.customElement;
// TODO remove all the ?? stuff, we don't need it now that we're validating the config // TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */ /** @type {ComponentAnalysis} */
const analysis = { const analysis = {
@ -429,8 +461,32 @@ export function analyze_component(root, source, options) {
module, module,
instance, instance,
template, template,
comments: root.comments,
elements: [], elements: [],
runes, runes,
// if we are not in runes mode but we have no reserved references ($$props, $$restProps)
// and no `export let` we might be in a wannabe runes component that is using runes in an external
// module...we need to fallback to the runic behavior
maybe_runes:
!runes &&
// if they explicitly disabled runes, use the legacy behavior
options.runes !== false &&
![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name)
) &&
!instance.ast.body.some(
(node) =>
node.type === 'LabeledStatement' ||
(node.type === 'ExportNamedDeclaration' &&
((node.declaration &&
node.declaration.type === 'VariableDeclaration' &&
node.declaration.kind === 'let') ||
node.specifiers.some(
(specifier) =>
specifier.local.type === 'Identifier' &&
instance.scope.get(specifier.local.name)?.declaration_kind === 'let'
)))
),
tracing: false, tracing: false,
classes: new Map(), classes: new Map(),
immutable: runes || options.immutable, immutable: runes || options.immutable,
@ -446,11 +502,11 @@ export function analyze_component(root, source, options) {
needs_props: false, needs_props: false,
event_directive_node: null, event_directive_node: null,
uses_event_attributes: false, uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement, custom_element: is_custom_element,
inject_styles: options.css === 'injected' || options.customElement, inject_styles: options.css === 'injected' || is_custom_element,
accessors: options.customElement accessors:
? true is_custom_element ||
: (runes ? false : !!options.accessors) || (runes ? false : !!options.accessors) ||
// because $set method needs accessors // because $set method needs accessors
options.compatibility?.componentApi === 4, options.compatibility?.componentApi === 4,
reactive_statements: new Map(), reactive_statements: new Map(),
@ -472,9 +528,17 @@ export function analyze_component(root, source, options) {
source, source,
undefined_exports: new Map(), undefined_exports: new Map(),
snippet_renderers: new Map(), snippet_renderers: new Map(),
snippets: new Set() snippets: new Set(),
async_deriveds: new Set()
}; };
state.adjust({
component_name: analysis.name,
dev: options.dev,
rootDir: options.rootDir,
runes
});
if (!runes) { if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export // every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) { for (const node of instance.ast.body) {

@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) {
} }
} }
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next(); context.next();
} }

@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted; return unhoisted;
} }
// If we are referencing a binding that is shadowed in another scope then bail out. // 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) { if (
local_binding !== null &&
binding !== null &&
local_binding.node !== binding.node &&
scope.declarations.get(reference) !== binding
) {
return unhoisted; return unhoisted;
} }

@ -41,5 +41,8 @@ export function AwaitBlock(node, context) {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
context.next(); context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
if (node.pending) context.visit(node.pending);
if (node.then) context.visit(node.then);
if (node.catch) context.visit(node.catch);
} }

@ -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, object, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js'; import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js'; import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
/** /**
* @param {CallExpression} node * @param {CallExpression} node
@ -219,6 +220,13 @@ export function CallExpression(node, context) {
break; break;
case '$effect.pending':
if (context.state.expression) {
context.state.expression.has_state = true;
}
break;
case '$inspect': case '$inspect':
if (node.arguments.length < 1) { if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments'); e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
@ -283,7 +291,19 @@ export function CallExpression(node, context) {
} }
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') { if (rune === '$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 }); context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else { } else {
context.next(); context.next();

@ -33,6 +33,9 @@ export function ClassBody(node, context) {
/** @type {Map<string, StateField>} */ /** @type {Map<string, StateField>} */
const state_fields = new Map(); const state_fields = new Map();
/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
const fields = new Map();
context.state.analysis.classes.set(node, state_fields); context.state.analysis.classes.set(node, state_fields);
/** @type {MethodDefinition | null} */ /** @type {MethodDefinition | null} */
@ -54,6 +57,14 @@ export function ClassBody(node, context) {
e.state_field_duplicate(node, name); e.state_field_duplicate(node, name);
} }
const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
const field = fields.get(_key);
// if there's already a method or assigned field, error
if (field && !(field.length === 1 && field[0] === 'prop')) {
e.duplicate_class_field(node, _key);
}
state_fields.set(name, { state_fields.set(name, {
node, node,
type: rune, type: rune,
@ -67,10 +78,48 @@ export function ClassBody(node, context) {
for (const child of node.body) { for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value); handle(child, child.key, child.value);
const key = /** @type {string} */ (get_name(child.key));
const field = fields.get(key);
if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
continue;
}
e.duplicate_class_field(child, key);
} }
if (child.type === 'MethodDefinition' && child.kind === 'constructor') { if (child.type === 'MethodDefinition') {
if (child.kind === 'constructor') {
constructor = child; constructor = child;
} else if (!child.computed) {
const key = (child.static ? '@' : '') + get_name(child.key);
const field = fields.get(key);
if (!field) {
fields.set(key, [child.kind]);
continue;
}
if (
field.includes(child.kind) ||
field.includes('prop') ||
field.includes('assigned_prop')
) {
e.duplicate_class_field(child, key);
}
if (child.kind === 'get') {
if (field.length === 1 && field[0] === 'set') {
field.push('get');
continue;
}
} else if (child.kind === 'set') {
if (field.length === 1 && field[0] === 'get') {
field.push('set');
continue;
}
} else {
field.push(child.kind);
continue;
}
e.duplicate_class_field(child, key);
}
} }
} }

@ -32,5 +32,8 @@ export function ConstTag(node, context) {
e.const_tag_invalid_placement(node); e.const_tag_invalid_placement(node);
} }
context.next(); const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
} }

@ -11,6 +11,17 @@ export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised // visit children, so bindings are correctly initialised
context.next(); 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') { if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let` // in runes mode, forbid `export let`
if ( if (

@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
// unfortunately this is necessary in order to fix invalid HTML // unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
context.next(); context.next({ ...context.state, expression: node.metadata.expression });
} }

@ -90,9 +90,13 @@ export function Identifier(node, context) {
if (binding) { if (binding) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.dependencies.add(binding); context.state.expression.dependencies.add(binding);
context.state.expression.references.add(binding);
context.state.expression.has_state ||= context.state.expression.has_state ||=
binding.kind !== 'static' && binding.kind !== 'static' &&
!binding.is_function() && (binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'rest_prop' ||
!binding.is_function()) &&
!context.state.scope.evaluate(node).is_known; !context.state.scope.evaluate(node).is_known;
} }

@ -17,5 +17,11 @@ export function IfBlock(node, context) {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
context.next(); context.visit(node.test, {
...context.state,
expression: node.metadata.expression
});
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
} }

@ -16,5 +16,6 @@ export function KeyBlock(node, context) {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
context.next(); context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
context.visit(node.fragment);
} }

@ -15,8 +15,9 @@ export function MemberExpression(node, context) {
} }
} }
if (context.state.expression && !is_pure(node, context)) { if (context.state.expression) {
context.state.expression.has_state = true; context.state.expression.has_member_expression = true;
context.state.expression.has_state ||= !is_pure(node, context);
} }
if (!is_safe_identifier(node, context.state.scope)) { if (!is_safe_identifier(node, context.state.scope)) {

@ -9,7 +9,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.js'; import { create_attribute, is_custom_element_node } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.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 { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';

@ -54,7 +54,7 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
context.visit(callee); context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) { for (const arg of expression.arguments) {
const metadata = create_expression_metadata(); const metadata = create_expression_metadata();

@ -32,6 +32,7 @@ export function StyleDirective(node, context) {
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; 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 { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
const valid = ['onerror', 'failed']; const valid = ['onerror', 'failed', 'pending'];
/** /**
* @param {AST.SvelteBoundary} node * @param {AST.SvelteBoundary} node

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.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 { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
@ -62,5 +62,17 @@ export function SvelteElement(node, context) {
mark_subtree_dynamic(context.path); 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 { visit_component } from './shared/component.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { filename } from '../../../state.js'; import { filename, UNKNOWN_FILENAME } from '../../../state.js';
/** /**
* @param {AST.SvelteSelf} node * @param {AST.SvelteSelf} node
@ -23,9 +23,9 @@ export function SvelteSelf(node, context) {
} }
if (context.state.analysis.runes) { 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 = const basename =
filename === '(unknown)' filename === UNKNOWN_FILENAME
? 'Self.svelte' ? 'Self.svelte'
: /** @type {string} */ (filename.split(/[/\\]/).pop()); : /** @type {string} */ (filename.split(/[/\\]/).pop());

@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
} }
} }
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next(); context.next();
} }

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

@ -13,6 +13,16 @@ export function visit_function(node, context) {
scope: context.state.scope scope: context.state.scope
}; };
if (context.state.expression) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
context.state.expression.references.add(binding);
}
}
}
context.next({ context.next({
...context.state, ...context.state,
function_depth: context.state.function_depth + 1, function_depth: context.state.function_depth + 1,

@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js'; import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BinaryExpression } from './visitors/BinaryExpression.js'; import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js'; import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js'; import { BlockStatement } from './visitors/BlockStatement.js';
@ -88,6 +89,7 @@ const visitors = {
AssignmentExpression, AssignmentExpression,
Attribute, Attribute,
AwaitBlock, AwaitBlock,
AwaitExpression,
BinaryExpression, BinaryExpression,
BindDirective, BindDirective,
BlockStatement, BlockStatement,
@ -162,15 +164,16 @@ export function client_component(analysis, options) {
state_fields: new Map(), state_fields: new Map(),
transform: {}, transform: {},
in_constructor: false, in_constructor: false,
in_derived: false,
instance_level_snippets: [], instance_level_snippets: [],
module_level_snippets: [], module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then // these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null), init: /** @type {any} */ (null),
update: /** @type {any} */ (null), update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null), after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null) template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
}; };
const module = /** @type {ESTree.Program} */ ( const module = /** @type {ESTree.Program} */ (
@ -206,7 +209,8 @@ export function client_component(analysis, options) {
/** @type {ESTree.Statement[]} */ /** @type {ESTree.Statement[]} */
const store_setup = []; const store_setup = [];
/** @type {ESTree.Statement} */
let store_init = b.empty;
/** @type {ESTree.VariableDeclaration[]} */ /** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = []; const legacy_reactive_declarations = [];
@ -224,8 +228,9 @@ export function client_component(analysis, options) {
if (binding.kind === 'store_sub') { if (binding.kind === 'store_sub') {
if (store_setup.length === 0) { if (store_setup.length === 0) {
needs_store_cleanup = true; needs_store_cleanup = true;
store_setup.push( store_init = b.const(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores')) 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)]; const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name)); if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([ let component_block = b.block([
...store_setup, ...store_setup,
...legacy_reactive_declarations, ...legacy_reactive_declarations,
...group_binding_declarations, ...group_binding_declarations,
@ -358,10 +363,67 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (instance.body), .../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context analysis.runes || !analysis.needs_context
? b.empty ? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)), : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
.../** @type {ESTree.Statement[]} */ (template.body)
]); ]);
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;
if (!analysis.runes) { if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x // Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) { for (const { name, alias } of analysis.exports) {
@ -389,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 // 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 // so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) { if (should_inject_context) {
@ -466,14 +516,6 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props')))); 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. // Merge hoisted statements into module body.
// Ensure imports are on top, with the order preserved, then module body, then hoisted statements // Ensure imports are on top, with the order preserved, then module body, then hoisted statements
/** @type {ESTree.ImportDeclaration[]} */ /** @type {ESTree.ImportDeclaration[]} */
@ -534,6 +576,10 @@ export function client_component(analysis, options) {
); );
} }
if (options.experimental.async) {
body.unshift(b.imports([], 'svelte/internal/flags/async'));
}
if (!analysis.runes) { if (!analysis.runes) {
body.unshift(b.imports([], 'svelte/internal/flags/legacy')); body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
} }
@ -575,8 +621,9 @@ export function client_component(analysis, options) {
); );
} }
if (analysis.custom_element) { const ce = options.customElementOptions ?? options.customElement;
const ce = analysis.custom_element;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};
/** @type {ESTree.Property[]} */ /** @type {ESTree.Property[]} */
@ -667,7 +714,9 @@ export function client_module(analysis, options) {
scopes: analysis.module.scopes, scopes: analysis.module.scopes,
state_fields: new Map(), state_fields: new Map(),
transform: {}, transform: {},
in_constructor: false in_constructor: false,
in_derived: false,
is_instance: false
}; };
const module = /** @type {ESTree.Program} */ ( const module = /** @type {ESTree.Program} */ (

@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js'; import type { Template } from './transform-template/template.js';
import type { Memoizer } from './visitors/shared/utils.js';
export interface ClientTransformState extends TransformState { export interface ClientTransformState extends TransformState {
/** /**
@ -20,6 +21,14 @@ export interface ClientTransformState extends TransformState {
*/ */
readonly in_constructor: boolean; 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< readonly transform: Record<
string, string,
{ {
@ -40,7 +49,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly options: ValidatedCompileOptions; readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>; readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>; readonly events: Set<string>;
readonly is_instance: boolean;
readonly store_to_invalidate?: string; readonly store_to_invalidate?: string;
/** Stuff that happens before the render effect(s) */ /** Stuff that happens before the render effect(s) */
@ -49,8 +57,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[]; readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** Expressions used inside the render effect */ /** Memoized expressions */
readonly expressions: Expression[]; readonly memoizer: Memoizer;
/** The HTML template string */ /** The HTML template string */
readonly template: Template; readonly template: Template;
readonly metadata: { readonly metadata: {

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

@ -1,21 +1,14 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_expression } from './shared/utils.js';
/** /**
* @param {AST.AttachTag} node * @param {AST.AttachTag} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function AttachTag(node, context) { export function AttachTag(node, context) {
context.state.init.push( const expression = build_expression(context, node.expression, node.metadata.expression);
b.stmt( context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
context.next(); context.next();
} }

@ -1,10 +1,11 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ /** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js'; import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.AwaitBlock} node * @param {AST.AwaitBlock} node
@ -14,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order // Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); const expression = b.thunk(
build_expression(context, node.expression, node.metadata.expression),
node.metadata.expression.has_await
);
let then_block; let then_block;
let catch_block; let catch_block;
@ -53,7 +57,7 @@ export function AwaitBlock(node, context) {
} }
context.state.init.push( context.state.init.push(
b.stmt( add_svelte_meta(
b.call( b.call(
'$.await', '$.await',
context.state.node, context.state.node,
@ -63,7 +67,9 @@ export function AwaitBlock(node, context) {
: b.null, : b.null,
then_block, then_block,
catch_block catch_block
) ),
node,
'await'
) )
); );
} }

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

@ -60,10 +60,11 @@ export function CallExpression(node, context) {
case '$derived': case '$derived':
case '$derived.by': { case '$derived.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0])); let fn = /** @type {Expression} */ (
if (rune === '$derived') fn = b.thunk(fn); 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': case '$state.snapshot':
@ -122,6 +123,9 @@ export function CallExpression(node, context) {
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg))) .../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
); );
case '$effect.pending':
return b.call('$.pending');
case '$inspect': case '$inspect':
case '$inspect().with': case '$inspect().with':
return transform_inspect_rune(node, context); return transform_inspect_rune(node, context);
@ -137,7 +141,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name node.callee.property.name
) && ) &&
node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases? node.arguments.some(
(arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown
)
) { ) {
return b.call( return b.call(
node.callee, node.callee,

@ -8,12 +8,6 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function Component(node, context) { export function Component(node, context) {
const component = build_component( const component = build_component(node, node.name, context);
node,
// if it's not dynamic we will just use the node name, if it is dynamic we will use the node name
// only if it's a valid identifier, otherwise we will use a default name
!node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component',
context
);
context.state.init.push(component); context.state.init.push(component);
} }

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */ /** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/** /**
* @param {AST.ConstTag} node * @param {AST.ConstTag} node
@ -15,15 +16,14 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
context.state.init.push( const init = build_expression(context, declaration.init, node.metadata.expression);
b.const( let expression = create_derived(context.state, b.thunk(init));
declaration.id,
create_derived( if (dev) {
context.state, expression = b.call('$.tag', expression, b.literal(declaration.id.name));
b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) }
)
) context.state.init.push(b.const(declaration.id, expression));
);
context.state.transform[declaration.id.name] = { read: get_value }; context.state.transform[declaration.id.name] = { read: get_value };
@ -48,18 +48,26 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y` // TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object // instead of destructuring it only to return a new object
const init = build_expression(
{ ...context, state: child_state },
declaration.init,
node.metadata.expression
);
const fn = b.arrow( const fn = b.arrow(
[], [],
b.block([ b.block([
b.const( b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]) ])
); );
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 // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */ /** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -12,8 +12,8 @@ import {
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js'; import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.EachBlock} node * @param {AST.EachBlock} node
@ -24,11 +24,18 @@ export function EachBlock(node, context) {
// expression should be evaluated in the parent scope, not the scope // expression should be evaluated in the parent scope, not the scope
// created by the each block itself // created by the each block itself
const collection = /** @type {Expression} */ ( const parent_scope_state = {
context.visit(node.expression, {
...context.state, ...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent) scope: /** @type {Scope} */ (context.state.scope.parent)
}) };
const collection = build_expression(
{
...context,
state: parent_scope_state
},
node.expression,
node.metadata.expression
); );
if (!each_node_meta.is_controlled) { if (!each_node_meta.is_controlled) {
@ -305,11 +312,9 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index)); declarations.push(b.let(node.index, index));
} }
if (dev && node.metadata.keyed) { const { has_await } = node.metadata.expression;
context.state.init.push( const get_collection = b.thunk(collection, has_await);
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
);
}
const render_args = [b.id('$$anchor'), item]; const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index); if (uses_index || collection_id) render_args.push(index);
@ -319,7 +324,7 @@ export function EachBlock(node, context) {
const args = [ const args = [
context.state.node, context.state.node,
b.literal(flags), b.literal(flags),
b.thunk(collection), thunk,
key_function, key_function,
b.arrow(render_args, b.block(declarations.concat(block.body))) b.arrow(render_args, b.block(declarations.concat(block.body)))
]; ];
@ -330,7 +335,26 @@ export function EachBlock(node, context) {
); );
} }
context.state.init.push(b.stmt(b.call('$.each', ...args))); 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);
}
} }
/** /**

@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
import { clean_nodes, infer_namespace } from '../../utils.js'; import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js'; import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js'; import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js'; import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js'; import { Template } from '../transform-template/template.js';
/** /**
@ -47,9 +47,7 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement'; const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template = const is_single_child_not_needing_template =
trimmed.length === 1 && trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' && trimmed[0].elseif));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
@ -64,8 +62,8 @@ export function Fragment(node, context) {
...context.state, ...context.state,
init: [], init: [],
update: [], update: [],
expressions: [],
after_update: [], after_update: [],
memoizer: new Memoizer(),
template: new Template(), template: new Template(),
transform: { ...context.state.transform }, transform: { ...context.state.transform },
metadata: { metadata: {

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function FunctionDeclaration(node, 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) { if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context); const params = build_hoisted_params(node, context);

@ -1,8 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js'; import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/** /**
* @param {AST.HtmlTag} node * @param {AST.HtmlTag} node
@ -11,7 +11,9 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) { export function HtmlTag(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
const expression = /** @type {Expression} */ (context.visit(node.expression)); 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_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml'; const is_mathml = context.state.metadata.namespace === 'mathml';
@ -20,7 +22,7 @@ export function HtmlTag(node, context) {
b.call( b.call(
'$.html', '$.html',
context.state.node, context.state.node,
b.thunk(expression), b.thunk(html),
is_svg && b.true, is_svg && b.true,
is_mathml && b.true, is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && 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 // push into init, so that bindings run afterwards, which might trigger another run and override hydration
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); context.state.init.push(statement);
} }
}

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.IfBlock} node * @param {AST.IfBlock} node
@ -12,35 +13,32 @@ export function IfBlock(node, context) {
const statements = []; const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); 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; let alternate_id;
if (node.alternate) { if (node.alternate) {
alternate_id = context.state.scope.generate('alternate');
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
const nodes = node.alternate.nodes; alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
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))); 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[]} */ /** @type {Expression[]} */
const args = [ const args = [
node.elseif ? b.id('$$anchor') : context.state.node, context.state.node,
b.arrow( b.arrow(
[b.id('$$render')], [b.id('$$render')],
b.block([ b.block([
b.if( b.if(
/** @type {Expression} */ (context.visit(node.test)), test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), b.stmt(b.call('$$render', consequent_id)),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false)))
) )
]) ])
) )
@ -68,10 +66,23 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the // ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it // transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local' // should play when `x` or `y` change — both are considered 'local'
args.push(b.id('$$elseif')); args.push(b.true);
} }
statements.push(b.stmt(b.call('$.if', ...args))); statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
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)); context.state.init.push(b.block(statements));
} }
}

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.KeyBlock} node * @param {AST.KeyBlock} node
@ -10,10 +11,28 @@ import * as b from '#compiler/builders';
export function KeyBlock(node, context) { export function KeyBlock(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.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)); const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push( let statement = add_svelte_meta(
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) 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);
}

@ -3,14 +3,14 @@
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js'; import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [ const modifiers = /** @type {const} */ ([
'stopPropagation', 'stopPropagation',
'stopImmediatePropagation', 'stopImmediatePropagation',
'preventDefault', 'preventDefault',
'self', 'self',
'trusted', 'trusted',
'once' 'once'
]; ]);
/** /**
* @param {AST.OnDirective} node * @param {AST.OnDirective} node

@ -22,13 +22,7 @@ import {
build_set_style build_set_style
} from './shared/element.js'; } from './shared/element.js';
import { process_children } from './shared/fragment.js'; import { process_children } from './shared/fragment.js';
import { import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js'; import { visit_event_attribute } from './shared/events.js';
/** /**
@ -200,9 +194,6 @@ export function RegularElement(node, context) {
const node_id = context.state.node; const node_id = context.state.node;
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */ /** If true, needs `__value` for inputs */
const needs_special_value_handling = const needs_special_value_handling =
node.name === 'option' || node.name === 'option' ||
@ -210,6 +201,9 @@ export function RegularElement(node, context) {
bindings.has('group') || bindings.has('group') ||
bindings.has('checked'); bindings.has('checked');
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) { if (is_event_attribute(attribute)) {
visit_event_attribute(attribute, context); visit_event_attribute(attribute, context);
@ -217,7 +211,6 @@ export function RegularElement(node, context) {
} }
if (needs_special_value_handling && attribute.name === 'value') { if (needs_special_value_handling && attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
continue; continue;
} }
@ -261,7 +254,9 @@ export function RegularElement(node, context) {
attribute.value, attribute.value,
context, context,
(value, metadata) => (value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value 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); const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -327,11 +322,15 @@ export function RegularElement(node, context) {
// (e.g. `<span>{location}</span>`), set `textContent` programmatically // (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content = const use_text_content =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') && 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'); trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) { if (use_text_content) {
const { value } = build_template_chunk(trimmed, context.visit, child_state); const { value } = build_template_chunk(trimmed, context, child_state);
const empty_string = value.type === 'Literal' && value.value === ''; const empty_string = value.type === 'Literal' && value.value === '';
if (!empty_string) { if (!empty_string) {
@ -392,6 +391,15 @@ export function RegularElement(node, context) {
context.state.update.push(b.stmt(b.assignment('=', dir, dir))); context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
} }
if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
}
}
}
context.state.template.pop_element(); context.state.template.pop_element();
} }
@ -453,54 +461,64 @@ function setup_select_synchronization(value_binding, context) {
/** /**
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier} * @return {ObjectExpression | Identifier}
*/ */
export function build_class_directives_object(class_directives, expressions, context) { export function build_class_directives_object(
class_directives,
context,
memoizer = context.state.memoizer
) {
let properties = []; let properties = [];
let has_call_or_state = false; let has_call_or_state = false;
let has_await = false;
for (const d of class_directives) { for (const d of class_directives) {
const expression = /** @type Expression */ (context.visit(d.expression)); const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression)); properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; 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); const directives = b.object(properties);
return has_call_or_state ? get_expression_id(expressions, directives) : directives; return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
} }
/** /**
* @param {AST.StyleDirective[]} style_directives * @param {AST.StyleDirective[]} style_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context * @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}} * @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/ */
export function build_style_directives_object(style_directives, expressions, context) { export function build_style_directives_object(
let normal_properties = []; style_directives,
let important_properties = []; context,
memoizer = context.state.memoizer
) {
const normal = b.object([]);
const important = b.object([]);
for (const directive of style_directives) { let has_call_or_state = false;
let has_await = false;
for (const d of style_directives) {
const expression = const expression =
directive.value === true d.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state) ? build_getter(b.id(d.name), context.state)
: build_attribute_value(directive.value, context, (value, metadata) => : build_attribute_value(d.value, context).value;
metadata.has_call ? get_expression_id(expressions, value) : value
).value; const object = d.modifiers.includes('important') ? important : normal;
const property = b.init(directive.name, expression); object.properties.push(b.init(d.name, expression));
if (directive.modifiers.includes('important')) { has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
important_properties.push(property); has_await ||= d.metadata.expression.has_await;
} else {
normal_properties.push(property);
}
} }
return important_properties.length const directives = important.properties.length ? b.array([normal, important]) : normal;
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties); return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
} }
/** /**
@ -622,12 +640,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute); element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(state, value)
: get_expression_id(state.expressions, value)
: value
); );
const evaluated = context.state.scope.evaluate(value); const evaluated = context.state.scope.evaluate(value);
@ -652,23 +665,21 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
: inner_assignment : inner_assignment
); );
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (has_state) { if (has_state) {
const id = state.scope.generate(`${node_id.name}_value`); const id = b.id(state.scope.generate(`${node_id.name}_value`));
build_update_assignment(
state,
id,
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined, // `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is // that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different. // initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined, const init = element === 'option' ? b.object([]) : undefined;
value,
update state.init.push(b.var(id, init));
); state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
} else { } else {
state.init.push(update); state.init.push(update);
} }
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id)));
}
} }

@ -1,8 +1,9 @@
/** @import { Expression } from 'estree' */ /** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js'; import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { add_svelte_meta, build_expression, Memoizer } from './shared/utils.js';
/** /**
* @param {AST.RenderTag} node * @param {AST.RenderTag} node
@ -11,27 +12,36 @@ import * as b from '#compiler/builders';
export function RenderTag(node, context) { export function RenderTag(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
const expression = unwrap_optional(node.expression); const call = unwrap_optional(node.expression);
const callee = expression.callee;
const raw_args = expression.arguments;
/** @type {Expression[]} */ /** @type {Expression[]} */
let args = []; let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i]))); const memoizer = new Memoizer();
const { has_call } = node.metadata.arguments[i];
for (let i = 0; i < call.arguments.length; i++) {
if (has_call) { const arg = /** @type {Expression} */ (call.arguments[i]);
const id = b.id(context.state.scope.generate('render_arg')); const metadata = node.metadata.arguments[i];
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id))); let expression = build_expression(context, arg, metadata);
} else {
args.push(thunk); if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
} }
args.push(b.thunk(expression));
} }
let snippet_function = /** @type {Expression} */ (context.visit(callee)); memoizer.apply();
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
let snippet_function = build_expression(
context,
/** @type {Expression} */ (call.callee),
node.metadata.expression
);
if (node.metadata.dynamic) { if (node.metadata.dynamic) {
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one
@ -39,18 +49,41 @@ export function RenderTag(node, context) {
snippet_function = b.logical('??', snippet_function, b.id('$.noop')); snippet_function = b.logical('??', snippet_function, b.id('$.noop'));
} }
context.state.init.push( statements.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
); );
} else { } else {
context.state.init.push( statements.push(
b.stmt( add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
context.state.node, context.state.node,
...args ...args
),
node,
'render'
)
);
}
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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js'; 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 * @param {AST.SlotElement} node
@ -22,7 +22,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
const lets = []; const lets = [];
let is_default = true; const memoizer = new Memoizer();
let name = b.literal('default'); let name = b.literal('default');
@ -33,12 +33,14 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value( const { value, has_state } = build_attribute_value(
attribute.value, attribute.value,
context, 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') { if (attribute.name === 'name') {
name = /** @type {Literal} */ (value); name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') { } else if (attribute.name !== 'slot') {
if (has_state) { if (has_state) {
props.push(b.get(attribute.name, [b.return(value)])); 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 // Let bindings first, they can be used on attributes
context.state.init.push(...lets); context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
const props_expression = const props_expression =
spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); 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.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call( statements.push(
'$.slot', b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
@ -71,7 +71,7 @@ export function SvelteBoundary(node, context) {
hoisted.push(snippet); 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)); props.properties.push(b.prop('init', child.expression, child.expression));
} }

@ -10,7 +10,7 @@ import {
build_attribute_effect, build_attribute_effect,
build_set_class build_set_class
} from './shared/element.js'; } from './shared/element.js';
import { build_render_statement } from './shared/utils.js'; import { build_render_statement, Memoizer } from './shared/utils.js';
/** /**
* @param {AST.SvelteElement} node * @param {AST.SvelteElement} node
@ -32,7 +32,7 @@ export function SvelteElement(node, context) {
const style_directives = []; const style_directives = [];
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
const lets = []; const statements = [];
// Create a temporary context which picks up the init/update statements. // Create a temporary context which picks up the init/update statements.
// They'll then be added to the function parameter of $.element // They'll then be added to the function parameter of $.element
@ -46,8 +46,8 @@ export function SvelteElement(node, context) {
node: element_id, node: element_id,
init: [], init: [],
update: [], update: [],
expressions: [], after_update: [],
after_update: [] memoizer: new Memoizer()
} }
}; };
@ -64,7 +64,7 @@ export function SvelteElement(node, context) {
} else if (attribute.type === 'StyleDirective') { } else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute); style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') { } else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') { } else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state)); const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state));
inner_context.state.after_update.push(b.stmt(handler)); 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 ( if (
attributes.length === 1 && attributes.length === 1 &&
attributes[0].type === 'Attribute' && 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) { const expression = /** @type {Expression} */ (context.visit(node.tag));
if (node.fragment.nodes.length > 0) { const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
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)));
}
/** @type {Statement[]} */ /** @type {Statement[]} */
const inner = inner_context.state.init; const inner = inner_context.state.init;
@ -123,9 +116,16 @@ export function SvelteElement(node, context) {
).body ).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); const location = dev && locator(node.start);
context.state.init.push( statements.push(
b.stmt( b.stmt(
b.call( b.call(
'$.element', '$.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));
}
} }

@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js';
export function TitleElement(node, context) { export function TitleElement(node, context) {
const { has_state, value } = build_template_chunk( const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes), /** @type {any} */ (node.fragment.nodes),
context.visit, context
context.state
); );
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));

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

@ -4,7 +4,7 @@
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_bind_this, memoize_expression, validate_binding } 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_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js'; import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js'; import { determine_slot } from '../../../../../utils/slot.js';
@ -43,6 +43,8 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */ /** @type {Record<string, Expression[]>} */
const events = {}; const events = {};
const memoizer = new Memoizer();
/** @type {Property[]} */ /** @type {Property[]} */
const custom_css_props = []; const custom_css_props = [];
@ -52,6 +54,15 @@ export function build_component(node, component_name, context) {
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
const binding_initializers = []; const binding_initializers = [];
const is_component_dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
// The variable name used for the component inside $.component()
const intermediate_name =
node.type === 'Component' && node.metadata.dynamic
? context.state.scope.generate(node.name)
: '$$component';
/** /**
* If this component has a slot property, it is a named slot within another component. In this case * If this component has a slot property, it is a named slot within another component. In this case
* the slot scope applies to the component itself, too, and not just its children. * the slot scope applies to the component itself, too, and not just its children.
@ -118,16 +129,15 @@ export function build_component(node, component_name, context) {
(events[attribute.name] ||= []).push(handler); (events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') { } else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute)); const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
let value = expression;
if (attribute.metadata.expression.has_call) { if (attribute.metadata.expression.has_state) {
const id = b.id(context.state.scope.generate('spread_element')); props_and_spreads.push(
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); b.thunk(
value = b.call('$.get', id); attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
} ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
props_and_spreads.push(b.thunk(value)); )
);
} else { } else {
props_and_spreads.push(expression); props_and_spreads.push(expression);
} }
@ -136,10 +146,12 @@ export function build_component(node, component_name, context) {
custom_css_props.push( custom_css_props.push(
b.init( b.init(
attribute.name, 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 // TODO put the derived in the local block
metadata.has_call ? memoize_expression(context.state, value) : value return metadata.has_call || metadata.has_await
).value ? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}).value
) )
); );
continue; continue;
@ -157,12 +169,14 @@ export function build_component(node, component_name, context) {
attribute.value, attribute.value,
context, context,
(value, metadata) => { (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, // 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 // 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}`) // child component (e.g. `active={i === index}`)
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { const should_wrap_in_derived =
metadata.has_await ||
get_attribute_chunks(attribute.value).some((n) => {
return ( return (
n.type === 'ExpressionTag' && n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' && n.expression.type !== 'Identifier' &&
@ -170,7 +184,9 @@ export function build_component(node, component_name, context) {
); );
}); });
return should_wrap_in_derived ? memoize_expression(context.state, value) : value; return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
} }
); );
@ -199,7 +215,7 @@ export function build_component(node, component_name, context) {
b.call( b.call(
'$$ownership_validator.binding', '$$ownership_validator.binding',
b.literal(binding.node.name), b.literal(binding.node.name),
b.id(component_name), b.id(is_component_dynamic ? intermediate_name : component_name),
b.thunk(expression) b.thunk(expression)
) )
) )
@ -275,7 +291,7 @@ export function build_component(node, component_name, context) {
); );
} }
push_prop(b.prop('get', b.call('$.attachment'), expression, true)); push_prop(b.prop('init', b.call('$.attachment'), expression, true));
} }
} }
@ -414,8 +430,8 @@ export function build_component(node, component_name, context) {
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will // will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here. // always be referenced through just the identifier here.
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic) is_component_dynamic
? component_name ? intermediate_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))), : /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id, node_id,
props_expression props_expression
@ -430,9 +446,9 @@ export function build_component(node, component_name, context) {
}; };
} }
const statements = [...snippet_declarations]; const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) { if (is_component_dynamic) {
const prev = fn; const prev = fn;
fn = (node_id) => { fn = (node_id) => {
@ -441,11 +457,11 @@ export function build_component(node, component_name, context) {
node_id, node_id,
b.thunk( b.thunk(
/** @type {Expression} */ ( /** @type {Expression} */ (
context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression) context.visit(node.type === 'Component' ? b.member_id(component_name) : node.expression)
) )
), ),
b.arrow( b.arrow(
[b.id('$$anchor'), b.id(component_name)], [b.id('$$anchor'), b.id(intermediate_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))])
) )
); );
@ -474,7 +490,23 @@ export function build_component(node, component_name, context) {
); );
} else { } else {
context.state.template.push_comment(); context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
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]; return statements.length > 1 ? b.block(statements) : statements[0];

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

Loading…
Cancel
Save