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
env:
CI: true
TestNoAsync:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm playwright install chromium
- run: pnpm test runtime-runes
env:
CI: true
SVELTE_NO_ASYNC: true
Lint:
permissions: {}
runs-on: ubuntu-latest

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

@ -7,6 +7,7 @@ packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js
packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.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/types
packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte
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
playgrounds/sandbox/src/*
**/node_modules
**/.svelte-kit

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

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

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

@ -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:
```bash
```sh
npx sv create myapp
cd myapp
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:
@ -119,7 +119,9 @@ class Todo {
}
```
> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
### Built-in classes
Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity).
## `$state.raw`

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

@ -221,6 +221,21 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
## `$effect.pending`
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)):
```svelte
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<p>pending promises: {$effect.pending()}</p>
{/if}
```
## `$effect.root`
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase.

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

@ -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:
```bash
```sh
npm install -D vitest
```
@ -129,12 +129,12 @@ test('Effect', () => {
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync();
expect(log.value).toEqual([0]);
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log.value).toEqual([0, 1]);
expect(log).toEqual([0, 1]);
});
cleanup();
@ -148,17 +148,13 @@ test('Effect', () => {
*/
export function logger(getValue) {
/** @type {any[]} */
let log = $state([]);
let log = [];
$effect(() => {
log.push(getValue());
});
return {
get value() {
return log;
}
};
return log;
}
```
@ -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):
```bash
```sh
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.
- 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 [`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.
In case this is a custom or experimental attribute/event, you can enhance the typings like this:
```ts
/// file: additional-svelte-typings.d.ts
declare namespace svelteHTML {
// enhance elements
interface IntrinsicElements {
'my-custom-element': { someattribute: string; 'on:event': (e: CustomEvent<any>) => void };
}
// enhance attributes
interface HTMLAttributes<T> {
// If you want to use the beforeinstallprompt event
onbeforeinstallprompt?: (event: any) => any;
// If you want to use myCustomAttribute={..} (note: all lowercase)
mycustomattribute?: any; // You can replace any with something more specific if you like
}
}
```
Then make sure that `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect.
You can also declare the typings by augmenting the `svelte/elements` module like this:
In case this is a custom or experimental attribute/event, you can enhance the typings by augmenting the `svelte/elements` module like this:
```ts
/// file: additional-svelte-typings.d.ts
import { HTMLButtonAttributes } from 'svelte/elements';
declare module 'svelte/elements' {
// add a new element
export interface SvelteHTMLElements {
'custom-button': HTMLButtonAttributes;
}
// allows for more granular control over what element to add the typings to
// add a new global attribute that is available on all html elements
export interface HTMLAttributes<T> {
globalattribute?: string;
}
// add a new attribute for button elements
export interface HTMLButtonAttributes {
veryexperimentalattribute?: string;
}
@ -294,3 +279,5 @@ declare module 'svelte/elements' {
export {}; // ensure this is not an ambient module, else types will be overridden instead of augmented
```
Then make sure that the `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect.

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

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

@ -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
```
### duplicate_class_field
```
`%name%` has already been declared
```
### each_item_invalid_assignment
```
@ -480,6 +486,12 @@ Expected token %token%
Expected whitespace
```
### experimental_async
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
```
### export_undefined
```
@ -534,6 +546,12 @@ The arguments keyword cannot be used within the template or at the top level of
%message%
```
### legacy_await_invalid
```
Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
```
### legacy_export_invalid
```

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

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

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

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

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

@ -1,5 +1,323 @@
# 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
### 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):
```bash
```sh
npx sv create my-app
cd my-app
npm install

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

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

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

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

@ -30,6 +30,10 @@
> The $ prefix is reserved, and cannot be used for variables and imports
## duplicate_class_field
> `%name%` has already been declared
## 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}`)
@ -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
## experimental_async
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
## export_undefined
> `%name%` is not defined
@ -98,6 +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
## legacy_await_invalid
> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead

@ -71,7 +71,7 @@ Some templating languages (including Svelte) will 'fix' HTML by turning `<span /
To automate this, run the dedicated migration:
```bash
```sh
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
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

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

@ -118,36 +118,40 @@ const bundle = await bundle_code(
).js.code
);
if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {
failed = true;
// eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`);
}
/**
* @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
console.error(`${case_name} not treeshakeable`);
failed = true;
if (!bundle.includes('component_context.l')) {
// eslint-disable-next-line no-console
console.error(`✅ Legacy code treeshakeable`);
} else {
failed = true;
let lines = bundle.slice(index - 500, index + 500).split('\n');
const target_line = lines.findIndex((line) => line.includes(string));
// mark the failed line
lines = lines
.map((line, i) => (i === target_line ? `> ${line}` : `| ${line}`))
.slice(target_line - 5, target_line + 6);
// eslint-disable-next-line no-console
console.error('The first failed line:\n' + lines.join('\n'));
return;
}
}
// eslint-disable-next-line no-console
console.error(`❌ Legacy code not treeshakeable`);
console.error(`${case_name} treeshakeable`);
}
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
console.error(`$inspect.trace code not treeshakeable`);
}
check_bundle('Hydration code', 'hydrate_node', 'hydrate_next');
check_bundle('Legacy code', 'component_context.l');
check_bundle('$inspect.trace', `'CreatedAt'`);
if (failed) {
// 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);
}

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

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

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

@ -281,6 +281,13 @@ declare namespace $effect {
*/
export function pre(fn: () => void | (() => void)): void;
/**
* Returns the number of promises that are pending in the current boundary, not including child boundaries.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pending
*/
export function pending(): number;
/**
* The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template.
*
@ -314,13 +321,13 @@ declare namespace $effect {
* let count = $state(0);
*
* const cleanup = $effect.root(() => {
* $effect(() => {
* console.log(count);
* })
* $effect(() => {
* console.log(count);
* })
*
* return () => {
* console.log('effect root cleanup');
* }
* return () => {
* console.log('effect root cleanup');
* }
* });
* </script>
*

@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) {
super(message);
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.
// 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.
this.#diagnostic = new CompileDiagnostic(code, message, position);
Object.assign(this, this.#diagnostic);
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`);
}
/**
* `%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}`)
* @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`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function experimental_async(node) {
e(node, 'experimental_async', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async`);
}
/**
* `%name%` is not defined
* @param {null | number | NodeLike} node
@ -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`);
}
/**
* Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function legacy_await_invalid(node) {
e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`);
}
/**
* Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node
@ -834,7 +864,9 @@ export function bind_invalid_expression(node) {
* @returns {never}
*/
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 { walk as zimmerframe_walk } from 'zimmerframe';
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 { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.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) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source);
@ -65,11 +63,10 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset({ warning: options.warningFilter, filename: options.filename });
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);
}
@ -97,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record<string, any>}
*/
// TODO 6.0 remove unused `filename`
/**
* 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 `filename` option is unused and will be removed in Svelte 6.0.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern, loose } = {}) {
export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
state.reset({ warning: () => false, filename: undefined });
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);

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

@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @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 { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
@ -53,13 +67,19 @@ export function parse(source, typescript, is_script) {
/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @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 { 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, {
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
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @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)) {
let a = start;
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'), '');
}
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 */
add_comments(ast) {
if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, {
_(node, { next, path }) {
let comment;

@ -8,6 +8,7 @@ import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
@ -21,12 +22,6 @@ export class Parser {
*/
template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
@ -65,7 +60,6 @@ export class Parser {
}
this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd();
let match_lang;
@ -87,6 +81,7 @@ export class Parser {
type: 'Root',
fragment: create_fragment(),
options: null,
comments: [],
metadata: {
ts: this.ts
}
@ -299,6 +294,8 @@ export class Parser {
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose);
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);
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;
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 (`?:`). Therefore replace that sequence with something that will not error.
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
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
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

@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
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;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
let i = parser.root.comments.length;
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) {
@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
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) {
const char = parser.template[index];

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

@ -115,6 +115,19 @@ const visitors = {
TSDeclareFunction() {
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) {
if (node.declare) {
return b.empty;

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

@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
case 'ClassSelector': {
if (
!attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
if (!attribute_matches(element, 'class', name, '~=', 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 === '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.name.toLowerCase() !== name.toLowerCase()) continue;
if (attribute.name.toLowerCase() !== name_lower) continue;
if (attribute.value === true) return operator === null;
if (expected_value === null) return true;
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);
@ -649,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
/** @type {string[]} */
let prev_values = [];
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
if (!current_possible_values) return true;

@ -3,6 +3,7 @@
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.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 { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
@ -75,6 +77,7 @@ import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
import * as state from '../../state.js';
/**
* @type {Visitors}
@ -138,6 +141,7 @@ const visitors = {
AttachTag,
Attribute,
AwaitBlock,
AwaitExpression,
BindDirective,
CallExpression,
ClassBody,
@ -209,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) {
body: []
};
const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent);
const { scope, scopes, has_await } = create_scopes(
ast,
root,
allow_reactive_declarations,
parent
);
return { ast, scope, scopes };
return { ast, scope, scopes, has_await };
}
/**
@ -231,12 +240,18 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots'];
/**
* @param {Program} ast
* @param {string} source
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
export function analyze_module(ast, options) {
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
export function analyze_module(source, options) {
/** @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) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -253,15 +268,23 @@ export function analyze_module(ast, options) {
/** @type {Analysis} */
const analysis = {
module: { ast, scope, scopes },
module: { ast, scope, scopes, has_await },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
};
state.adjust({
dev: options.dev,
rootDir: options.rootDir,
runes: true
});
walk(
/** @type {Node} */ (ast),
{
@ -298,7 +321,12 @@ export function analyze_component(root, source, options) {
const module = js(root.module, scope_root, false, null);
const instance = js(root.instance, scope_root, true, module.scope);
const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);
const { scope, scopes, has_await } = create_scopes(
root.fragment,
scope_root,
false,
instance.scope
);
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
@ -406,7 +434,9 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
const runes =
options.runes ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) {
for (let check of synthetic_stores_legacy_check) {
@ -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
/** @type {ComponentAnalysis} */
const analysis = {
@ -429,8 +461,32 @@ export function analyze_component(root, source, options) {
module,
instance,
template,
comments: root.comments,
elements: [],
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,
classes: new Map(),
immutable: runes || options.immutable,
@ -446,13 +502,13 @@ export function analyze_component(root, source, options) {
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
? true
: (runes ? false : !!options.accessors) ||
// because $set method needs accessors
options.compatibility?.componentApi === 4,
custom_element: is_custom_element,
inject_styles: options.css === 'injected' || is_custom_element,
accessors:
is_custom_element ||
(runes ? false : !!options.accessors) ||
// because $set method needs accessors
options.compatibility?.componentApi === 4,
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
@ -472,9 +528,17 @@ export function analyze_component(root, source, options) {
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
snippets: new Set(),
async_deriveds: new Set()
};
state.adjust({
component_name: analysis.name,
dev: options.dev,
rootDir: options.rootDir,
runes
});
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
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();
}

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

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

@ -33,6 +33,9 @@ export function ClassBody(node, context) {
/** @type {Map<string, StateField>} */
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);
/** @type {MethodDefinition | null} */
@ -54,6 +57,14 @@ export function ClassBody(node, context) {
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, {
node,
type: rune,
@ -67,10 +78,48 @@ export function ClassBody(node, context) {
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
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') {
constructor = child;
if (child.type === 'MethodDefinition') {
if (child.kind === 'constructor') {
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);
}
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
context.next();
if (
context.state.ast_type &&
node.specifiers.some((specifier) =>
specifier.exported.type === 'Identifier'
? specifier.exported.name === 'default'
: specifier.exported.value === 'default'
)
) {
e.module_illegal_default_export(node);
}
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (

@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
// unfortunately this is necessary in order to fix invalid HTML
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 (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.references.add(binding);
context.state.expression.has_state ||=
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;
}

@ -17,5 +17,11 @@ export function IfBlock(node, context) {
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);
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)) {
context.state.expression.has_state = true;
if (context.state.expression) {
context.state.expression.has_member_expression = true;
context.state.expression.has_state ||= !is_pure(node, context);
}
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 { create_attribute, is_custom_element_node } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';

@ -54,7 +54,7 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
context.visit(callee);
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
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_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.has_await ||= chunk.metadata.expression.has_await;
}
}
}

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

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

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

@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
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
};
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.state,
function_depth: context.state.function_depth + 1,

@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
@ -88,6 +89,7 @@ const visitors = {
AssignmentExpression,
Attribute,
AwaitBlock,
AwaitExpression,
BinaryExpression,
BindDirective,
BlockStatement,
@ -162,15 +164,16 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @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} */ (
@ -206,7 +209,8 @@ export function client_component(analysis, options) {
/** @type {ESTree.Statement[]} */
const store_setup = [];
/** @type {ESTree.Statement} */
let store_init = b.empty;
/** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = [];
@ -224,8 +228,9 @@ export function client_component(analysis, options) {
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
needs_store_cleanup = true;
store_setup.push(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
store_init = b.const(
b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]),
b.call('$.setup_stores')
);
}
@ -350,7 +355,7 @@ export function client_component(analysis, options) {
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
let component_block = b.block([
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
@ -358,10 +363,67 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
.../** @type {ESTree.Statement[]} */ (template.body)
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
]);
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
if (analysis.instance.has_await) {
const params = [b.id('$$anchor')];
if (should_inject_props) {
params.push(b.id('$$props'));
}
if (store_setup.length > 0) {
params.push(b.id('$$stores'));
}
const body = b.function_declaration(
b.id('$$body'),
params,
b.block([
b.var('$$unsuspend', b.call('$.suspend')),
...component_block.body,
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body),
b.stmt(b.call('$$unsuspend'))
]),
true
);
state.hoisted.push(body);
component_block = b.block([
b.var('fragment', b.call('$.comment')),
b.var('node', b.call('$.first_child', b.id('fragment'))),
store_init,
b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))),
b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment')))
]);
} else {
component_block.body.unshift(store_init);
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
// trick esrap into including comments
component_block.loc = instance.loc;
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
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
// so that it can effectively clean up the store subscription even after the user effects runs
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'))));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
// Merge hoisted statements into module body.
// Ensure imports are on top, with the order preserved, then module body, then hoisted statements
/** @type {ESTree.ImportDeclaration[]} */
@ -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) {
body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
}
@ -575,8 +621,9 @@ export function client_component(analysis, options) {
);
}
if (analysis.custom_element) {
const ce = analysis.custom_element;
const ce = options.customElementOptions ?? options.customElement;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};
/** @type {ESTree.Property[]} */
@ -667,7 +714,9 @@ export function client_module(analysis, options) {
scopes: analysis.module.scopes,
state_fields: new Map(),
transform: {},
in_constructor: false
in_constructor: false,
in_derived: false,
is_instance: false
};
const module = /** @type {ESTree.Program} */ (

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

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

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

@ -1,10 +1,11 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @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 { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -14,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// 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 catch_block;
@ -53,7 +57,7 @@ export function AwaitBlock(node, context) {
}
context.state.init.push(
b.stmt(
add_svelte_meta(
b.call(
'$.await',
context.state.node,
@ -63,7 +67,9 @@ export function AwaitBlock(node, context) {
: b.null,
then_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.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (rune === '$derived') fn = b.thunk(fn);
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
return b.call('$.derived', fn);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
case '$state.snapshot':
@ -122,6 +123,9 @@ export function CallExpression(node, context) {
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
);
case '$effect.pending':
return b.call('$.pending');
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
@ -137,7 +141,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
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(
node.callee,

@ -8,12 +8,6 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
const component = build_component(
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
);
const component = build_component(node, node.name, context);
context.state.init.push(component);
}

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
@ -15,15 +16,14 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
context.state.init.push(
b.const(
declaration.id,
create_derived(
context.state,
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
)
)
);
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, b.thunk(init));
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.init.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
@ -48,18 +48,26 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// 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(
[],
b.block([
b.const(
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
),
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
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
// '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 { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -12,8 +12,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -24,11 +24,18 @@ export function EachBlock(node, context) {
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
const parent_scope_state = {
...context.state,
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) {
@ -305,11 +312,9 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
if (dev && node.metadata.keyed) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
const { has_await } = node.metadata.expression;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -319,7 +324,7 @@ export function EachBlock(node, context) {
const args = [
context.state.node,
b.literal(flags),
b.thunk(collection),
thunk,
key_function,
b.arrow(render_args, b.block(declarations.concat(block.body)))
];
@ -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 { transform_template } from '../transform-template/index.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';
/**
@ -47,9 +47,7 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' ||
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' && trimmed[0].elseif));
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
@ -64,8 +62,8 @@ export function Fragment(node, context) {
...context.state,
init: [],
update: [],
expressions: [],
after_update: [],
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
metadata: {

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

@ -1,8 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -11,7 +11,9 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
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_mathml = context.state.metadata.namespace === 'mathml';
@ -20,7 +22,7 @@ export function HtmlTag(node, context) {
b.call(
'$.html',
context.state.node,
b.thunk(expression),
b.thunk(html),
is_svg && b.true,
is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && b.true
@ -28,5 +30,18 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push(statement);
if (node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
)
)
);
} else {
context.state.init.push(statement);
}
}

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

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

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

@ -22,13 +22,7 @@ import {
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
@ -200,16 +194,16 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
node.name === 'select' ||
bindings.has('group') ||
bindings.has('checked');
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
node.name === 'select' ||
bindings.has('group') ||
bindings.has('checked');
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
visit_event_attribute(attribute, context);
@ -217,7 +211,6 @@ export function RegularElement(node, context) {
}
if (needs_special_value_handling && attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
continue;
}
@ -261,7 +254,9 @@ export function RegularElement(node, context) {
attribute.value,
context,
(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);
@ -327,11 +322,15 @@ export function RegularElement(node, context) {
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.every(
(node) =>
node.type === 'Text' ||
(!node.metadata.expression.has_state && !node.metadata.expression.has_await)
) &&
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
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 === '';
if (!empty_string) {
@ -392,6 +391,15 @@ export function RegularElement(node, context) {
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();
}
@ -453,54 +461,64 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @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 has_call_or_state = false;
let has_await = false;
for (const d of class_directives) {
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state ? 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 {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(style_directives, expressions, context) {
let normal_properties = [];
let important_properties = [];
export function build_style_directives_object(
style_directives,
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 =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(expressions, value) : value
).value;
const property = b.init(directive.name, expression);
if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {
normal_properties.push(property);
}
d.value === true
? build_getter(b.id(d.name), context.state)
: build_attribute_value(d.value, context).value;
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
return important_properties.length
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties);
const directives = important.properties.length ? b.array([normal, important]) : normal;
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);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(state, value)
: get_expression_id(state.expressions, value)
: value
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : 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
);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (has_state) {
const 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,
// 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.
element === 'option' ? b.object([]) : undefined,
value,
update
);
const id = b.id(state.scope.generate(`${node_id.name}_value`));
// `<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
// initially undefined, we need to set a value that is guaranteed to be different.
const init = element === 'option' ? b.object([]) : undefined;
state.init.push(b.var(id, init));
state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
} else {
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 { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { add_svelte_meta, build_expression, Memoizer } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -11,27 +12,36 @@ import * as b from '#compiler/builders';
export function RenderTag(node, context) {
context.state.template.push_comment();
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const raw_args = expression.arguments;
const call = unwrap_optional(node.expression);
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i])));
const { has_call } = node.metadata.arguments[i];
if (has_call) {
const id = b.id(context.state.scope.generate('render_arg'));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id)));
} else {
args.push(thunk);
const memoizer = new Memoizer();
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
const metadata = node.metadata.arguments[i];
let expression = build_expression(context, arg, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
}
args.push(b.thunk(expression));
}
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 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'));
}
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
statements.push(
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
);
} else {
context.state.init.push(
b.stmt(
statements.push(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...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 { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js';
import { memoize_expression } from './shared/utils.js';
import { Memoizer } from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@ -22,7 +22,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */
const lets = [];
let is_default = true;
const memoizer = new Memoizer();
let name = b.literal('default');
@ -33,12 +33,14 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value)
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value
);
if (attribute.name === 'name') {
name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') {
if (has_state) {
props.push(b.get(attribute.name, [b.return(value)]));
@ -51,9 +53,14 @@ export function SlotElement(node, context) {
}
}
memoizer.apply();
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
const props_expression =
spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads);
@ -62,14 +69,24 @@ export function SlotElement(node, context) {
? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
'$.slot',
context.state.node,
b.id('$$props'),
name,
props_expression,
fallback
statements.push(
b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
);
context.state.init.push(b.stmt(slot));
const async_values = memoizer.async_values();
if (async_values) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
async_values,
b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}

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

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

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

@ -4,7 +4,7 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -43,6 +43,8 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */
const events = {};
const memoizer = new Memoizer();
/** @type {Property[]} */
const custom_css_props = [];
@ -52,6 +54,15 @@ export function build_component(node, component_name, context) {
/** @type {ExpressionStatement[]} */
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
* 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);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
let value = expression;
if (attribute.metadata.expression.has_call) {
const id = b.id(context.state.scope.generate('spread_element'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id);
}
props_and_spreads.push(b.thunk(value));
if (attribute.metadata.expression.has_state) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
);
} else {
props_and_spreads.push(expression);
}
@ -136,10 +146,12 @@ export function build_component(node, component_name, context) {
custom_css_props.push(
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) =>
build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block
metadata.has_call ? memoize_expression(context.state, value) : value
).value
return metadata.has_call || metadata.has_await
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}).value
)
);
continue;
@ -157,20 +169,24 @@ export function build_component(node, component_name, context) {
attribute.value,
context,
(value, metadata) => {
if (!metadata.has_state) return value;
if (!metadata.has_state && !metadata.has_await) return value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component (e.g. `active={i === index}`)
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
const should_wrap_in_derived =
metadata.has_await ||
get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
return should_wrap_in_derived
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}
);
@ -199,7 +215,7 @@ export function build_component(node, component_name, context) {
b.call(
'$$ownership_validator.binding',
b.literal(binding.node.name),
b.id(component_name),
b.id(is_component_dynamic ? intermediate_name : component_name),
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
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)
? component_name
is_component_dynamic
? intermediate_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
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;
fn = (node_id) => {
@ -441,11 +457,11 @@ export function build_component(node, component_name, context) {
node_id,
b.thunk(
/** @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.id('$$anchor'), b.id(component_name)],
[b.id('$$anchor'), b.id(intermediate_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))])
)
);
@ -474,7 +490,23 @@ export function build_component(node, component_name, context) {
);
} else {
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];

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

Loading…
Cancel
Save