Merge branch 'main' into parallelize-async-work

parallelize-async-work
ComputerGuy 2 months ago
commit 3e54616f5b

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: replace `undefined` with `void(0)` in CallExpressions

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: place store setup inside async body

@ -4,6 +4,8 @@ on:
issue_comment:
types: [created]
permissions: {}
jobs:
trigger:
runs-on: ubuntu-latest
@ -15,7 +17,6 @@ jobs:
contents: read # to clone the repo
steps:
- 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

@ -14,7 +14,6 @@ jobs:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
with:

@ -1,6 +1,8 @@
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
permissions: {}

@ -17,7 +17,6 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@v4
with:
@ -27,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 24.x
cache: pnpm
- name: Install
@ -45,4 +44,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
packages/svelte/src/internal/server/warnings.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started:
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started:
- Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues).
- Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests).
@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree
#### Writing tests
All tests are located in `/test` folder.
All tests are located in the `/tests` folder.
Test samples are kept in `/test/xxx/samples` folder.
Test samples are kept in `/tests/xxx/samples` folders.
#### Running tests

@ -15,11 +15,11 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](/packages#routing) as well.
>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
There are also [plugins for other bundlers](/packages#bundler-plugins), but we recommend Vite.
## Editor tooling

@ -95,7 +95,7 @@ Since 5.6.0, if an `<input>` has a `defaultValue` and is part of a form, it will
## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`:
Checkbox inputs can be bound with `bind:checked`:
```svelte
<label>
@ -117,6 +117,8 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form>
```
> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
## `<input bind:indeterminate>`
Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:

@ -23,24 +23,6 @@ export default {
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)...
@ -99,7 +81,9 @@ let b = $derived(await two());
## 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).
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
@ -133,12 +117,28 @@ async function onclick() {
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
## Server-side rendering
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = +++await+++ render(App);
```
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
## 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).

@ -4,7 +4,7 @@ title: Testing
Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like [Vitest](https://vitest.dev/), [Jasmine](https://jasmine.github.io/), [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/).
## Unit and integration testing using Vitest
## Unit and component tests with Vitest
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
@ -160,9 +160,9 @@ export function logger(getValue) {
### Component testing
It is possible to test your components in isolation using Vitest.
It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component.
To get started, install jsdom (a library that shims DOM APIs):
@ -246,7 +246,49 @@ test('Component', async () => {
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
## E2E tests using Playwright
## Component tests with Storybook
[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
To get started, first install Storybook ([using Svelte's CLI](/docs/cli/storybook)) in your project via `npx sv add storybook` and choose the recommended configuration that includes testing features. If you're already using Storybook, and for more information on Storybook's testing capabilities, follow the [Storybook testing docs](https://storybook.js.org/docs/writing-tests?renderer=svelte) to get started.
You can create stories for component variations and test interactions with the [play function](https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=svelte#writing-interaction-tests), which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here's an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
```svelte
/// file: LoginForm.stories.svelte
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, fn } from 'storybook/test';
import LoginForm from './LoginForm.svelte';
const { Story } = defineMeta({
component: LoginForm,
args: {
// Pass a mock function to the `onSubmit` prop
onSubmit: fn(),
}
});
</script>
<Story name="Empty Form" />
<Story
name="Filled Form"
play={async ({ args, canvas, userEvent }) => {
// Simulate a user filling out the form
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
await userEvent.click(canvas.getByRole('button'));
// Run assertions
await expect(args.onSubmit).toHaveBeenCalledTimes(1);
await expect(canvas.getByText('Youre in!')).toBeInTheDocument();
}}
/>
```
## End-to-end tests with Playwright
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).

@ -65,7 +65,7 @@ There will be a blog post about this eventually, but in the meantime, check out
## Is there a UI component library?
There are several UI component libraries as well as standalone components. Find them under the [design systems section of the components page](https://sveltesociety.dev/packages?category=design-system) on the Svelte Society website.
There are several [UI component libraries](/packages#component-libraries) as well as standalone components listed on [the packages page](/packages).
## How do I test Svelte apps?
@ -91,17 +91,9 @@ Some resources for getting started with testing:
## Is there a router?
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React.
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React and Nuxt.js for Vue.
However, you can use any router library. A lot of people use [page.js](https://github.com/visionmedia/page.js). There's also [navaid](https://github.com/lukeed/navaid), which is very similar. And [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic with child routes, but without built-in history support.
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers).
However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).
## How do I write a mobile app with Svelte?

@ -312,6 +312,27 @@ 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.
### state_proxy_unmount
```
Tried to unmount a state proxy, rather than a component
```
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
### svelte_boundary_reset_noop
```

@ -81,7 +81,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
### a11y_consider_explicit_label
```
Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
```
### a11y_distracting_elements

@ -1,5 +1,19 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_invalid
```
Encountered asynchronous work while rendering synchronously.
```
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
### html_deprecated
```
The `html` property of server render results has been deprecated. Use `body` instead.
```
### lifecycle_function_unavailable
```

@ -0,0 +1,9 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_ssr
```
Attempted to use asynchronous rendering without `experimental.async` enabled
```
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -1,25 +1,5 @@
<!-- 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
```

@ -94,6 +94,7 @@ export default [
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/src/internal/server/warnings.js',
'packages/svelte/compiler/index.js',
// stuff we don't want to lint
'benchmarking/**',

@ -26,10 +26,11 @@
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@changesets/cli": "^2.29.7",
"@sveltejs/eslint-config": "^8.3.3",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",

@ -1,5 +1,127 @@
# svelte
## 5.39.8
### Patch Changes
- fix: check boundary `pending` attribute at runtime on server ([#16855](https://github.com/sveltejs/svelte/pull/16855))
- fix: preserve tuple type in `$state.snapshot` ([#16864](https://github.com/sveltejs/svelte/pull/16864))
- fix: allow await in svelte:boundary without pending ([#16857](https://github.com/sveltejs/svelte/pull/16857))
- fix: update `bind:checked` error message to clarify usage with radio inputs ([#16874](https://github.com/sveltejs/svelte/pull/16874))
## 5.39.7
### Patch Changes
- chore: simplify batch logic ([#16847](https://github.com/sveltejs/svelte/pull/16847))
- fix: rebase pending batches when other batches are committed ([#16866](https://github.com/sveltejs/svelte/pull/16866))
- fix: wrap async `children` in `$$renderer.async` ([#16862](https://github.com/sveltejs/svelte/pull/16862))
- fix: silence label warning for buttons and anchor tags with title attributes ([#16872](https://github.com/sveltejs/svelte/pull/16872))
- fix: coerce nullish `<title>` to empty string ([#16863](https://github.com/sveltejs/svelte/pull/16863))
## 5.39.6
### Patch Changes
- fix: depend on reads of deriveds created within reaction (async mode) ([#16823](https://github.com/sveltejs/svelte/pull/16823))
- fix: SSR regression of processing attributes of `<select>` and `<option>` ([#16821](https://github.com/sveltejs/svelte/pull/16821))
- fix: async `class:` + spread attributes were compiled into sync server-side code ([#16834](https://github.com/sveltejs/svelte/pull/16834))
- fix: ensure tick resolves within a macrotask ([#16825](https://github.com/sveltejs/svelte/pull/16825))
## 5.39.5
### Patch Changes
- fix: allow `{@html await ...}` and snippets with async content on the server ([#16817](https://github.com/sveltejs/svelte/pull/16817))
- fix: use nginx SSI-compatible comments for `$props.id()` ([#16820](https://github.com/sveltejs/svelte/pull/16820))
## 5.39.4
### Patch Changes
- fix: restore hydration state after `await` in `<script>` ([#16806](https://github.com/sveltejs/svelte/pull/16806))
## 5.39.3
### Patch Changes
- fix: remove outer hydration markers ([#16800](https://github.com/sveltejs/svelte/pull/16800))
- fix: async hydration ([#16797](https://github.com/sveltejs/svelte/pull/16797))
## 5.39.2
### Patch Changes
- fix: preserve SSR context when block expressions contain `await` ([#16791](https://github.com/sveltejs/svelte/pull/16791))
- chore: bump some devDependencies ([#16787](https://github.com/sveltejs/svelte/pull/16787))
## 5.39.1
### Patch Changes
- fix: issue `state_proxy_unmount` warning when unmounting a state proxy ([#16747](https://github.com/sveltejs/svelte/pull/16747))
- fix: add `then` to class component `render` output ([#16783](https://github.com/sveltejs/svelte/pull/16783))
## 5.39.0
### Minor Changes
- feat: experimental async SSR ([#16748](https://github.com/sveltejs/svelte/pull/16748))
### Patch Changes
- fix: correctly SSR hidden="until-found" ([#16773](https://github.com/sveltejs/svelte/pull/16773))
## 5.38.10
### Patch Changes
- fix: flush effects scheduled during boundary's pending phase ([#16738](https://github.com/sveltejs/svelte/pull/16738))
## 5.38.9
### Patch Changes
- chore: generate CSS hash using the filename ([#16740](https://github.com/sveltejs/svelte/pull/16740))
- fix: correctly analyze `<object.property>` components ([#16711](https://github.com/sveltejs/svelte/pull/16711))
- fix: clean up scheduling system ([#16741](https://github.com/sveltejs/svelte/pull/16741))
- fix: transform input defaults from spread ([#16481](https://github.com/sveltejs/svelte/pull/16481))
- fix: don't destroy contents of `svelte:boundary` unless the boundary is an error boundary ([#16746](https://github.com/sveltejs/svelte/pull/16746))
## 5.38.8
### Patch Changes
- fix: send `$effect.pending` count to the correct boundary ([#16732](https://github.com/sveltejs/svelte/pull/16732))
## 5.38.7
### Patch Changes
- fix: replace `undefined` with `void(0)` in CallExpressions ([#16693](https://github.com/sveltejs/svelte/pull/16693))
- fix: ensure batch exists when resetting a failed boundary ([#16698](https://github.com/sveltejs/svelte/pull/16698))
- fix: place store setup inside async body ([#16687](https://github.com/sveltejs/svelte/pull/16687))
## 5.38.6
### Patch Changes

@ -272,6 +272,25 @@ 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.
## state_proxy_unmount
> Tried to unmount a state proxy, rather than a component
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called

@ -66,7 +66,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
## a11y_consider_explicit_label
> Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
> Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
## a11y_distracting_elements

@ -0,0 +1,15 @@
## await_invalid
> Encountered asynchronous work while rendering synchronously.
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
## html_deprecated
> The `html` property of server render results has been deprecated. Use `body` instead.
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -1,5 +0,0 @@
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -0,0 +1,5 @@
## experimental_async_ssr
> Attempted to use asynchronous rendering without `experimental.async` enabled
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -1,21 +1,3 @@
## 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.38.6",
"version": "5.39.8",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -158,7 +158,7 @@
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.5",
"esbuild": "^0.21.5",
"esbuild": "^0.25.10",
"rollup": "^4.22.4",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",

@ -401,6 +401,7 @@ function run() {
transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.js');
transform('server-warnings', 'src/internal/server/warnings.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');

@ -0,0 +1,20 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {string} PARAMETER
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -85,6 +85,8 @@ declare namespace $state {
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends readonly unknown[]
? { [K in keyof T]: Snapshot<T[K]> }
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object

@ -177,7 +177,8 @@ export default function element(parser) {
mathml: false,
scoped: false,
has_spread: false,
path: []
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({

@ -278,7 +278,8 @@ export function analyze_module(source, options) {
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
classes: new Map(),
pickled_awaits: new Set()
};
state.adjust({
@ -304,7 +305,8 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
reactive_statement: null,
in_derived: false
},
visitors
);
@ -456,10 +458,19 @@ export function analyze_component(root, source, options) {
const is_custom_element = !!options.customElementOptions || options.customElement;
const name = module.scope.generate(options.name ?? component_name);
state.adjust({
component_name: name,
dev: options.dev,
rootDir: options.rootDir,
runes
});
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */
const analysis = {
name: module.scope.generate(options.name ?? component_name),
name,
root: scope_root,
module,
instance,
@ -520,7 +531,7 @@ export function analyze_component(root, source, options) {
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename,
filename: state.filename,
name: component_name,
hash
})
@ -531,16 +542,10 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
async_deriveds: new Set(),
pickled_awaits: 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) {
@ -697,7 +702,8 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
reactive_statement: null,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -764,7 +770,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth
function_depth: scope.function_depth,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -27,6 +27,11 @@ export interface AnalysisState {
// legacy stuff
reactive_statement: null | ReactiveStatement;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
in_derived: boolean;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,5 +1,6 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js';
/**
@ -7,16 +8,25 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
const tla = context.state.ast_type === 'instance' && context.state.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.path, context.state.in_derived) &&
!is_last_evaluated_expression(context.path, node))
) {
context.state.analysis.pickled_awaits.add(node);
}
let suspend = tla;
if (context.state.expression) {
context.state.expression.has_await = true;
if (
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}
@ -37,3 +47,101 @@ export function AwaitExpression(node, context) {
context.next();
}
/**
* @param {AST.SvelteNode[]} path
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) {
return true;
}
let i = path.length;
while (i--) {
const parent = 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 {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
export function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (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;
}
}

@ -33,7 +33,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
property.valid_elements.map((valid_element) => `\`<${valid_element}>\``).join(', ')
);
}
@ -67,11 +67,15 @@ export function BindDirective(node, context) {
}
} else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
e.bind_invalid_target(
node,
node.name,
`\`<input type="checkbox">\`${type?.value[0].data === 'radio' ? ` — for \`<input type="radio">\`, use \`bind:group\`` : ''}`
);
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
e.bind_invalid_target(node, node.name, '<input type="file">');
e.bind_invalid_target(node, node.name, '`<input type="file">`');
}
}
}
@ -94,7 +98,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
`non-\`<svg>\` elements. Use \`bind:clientWidth\` for \`<svg>\` instead`
);
}

@ -241,6 +241,7 @@ export function CallExpression(node, context) {
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
in_derived: true,
expression
});

@ -35,5 +35,9 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
in_derived: true
});
}

@ -70,7 +70,7 @@ export function RegularElement(node, context) {
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
node.metadata.synthetic_value_node = child;
}
const binding = context.state.scope.get(node.name);

@ -49,6 +49,12 @@ export function VariableDeclarator(node, context) {
}
}
if (rune === '$derived') {
context.visit(node.id);
context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true });
return;
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);

@ -382,7 +382,10 @@ export function check_element(node, context) {
}
// element-specific checks
const is_labelled = attribute_map.has('aria-label') || attribute_map.has('aria-labelledby');
const is_labelled =
attribute_map.has('aria-label') ||
attribute_map.has('aria-labelledby') ||
attribute_map.has('title');
switch (node.name) {
case 'a':

@ -166,7 +166,6 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
@ -714,7 +713,6 @@ export function client_module(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
is_instance: false
};

@ -22,11 +22,6 @@ 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;

@ -5,7 +5,7 @@
/** @import { Scope } from '../../scope.js' */
/** @import { Visitor } from 'zimmerframe' */
import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js';
import { is_simple_expression, save } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@ -360,7 +360,7 @@ export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk))));
return save(b.call('$.async_derived', thunk));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}

@ -1,6 +1,7 @@
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { save } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
/**
@ -10,13 +11,8 @@ import * as b from '../../../../utils/builders.js';
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)));
if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
}
// in dev, note which values are read inside a reactive expression,
@ -27,100 +23,3 @@ export function AwaitExpression(node, context) {
return argument === node.argument ? node : { ...node, argument };
}
/**
* @param {Context} context
*/
function is_reactive_expression(context) {
if (context.state.in_derived) {
return true;
}
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {Context} context
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(context, node) {
let i = context.path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -44,9 +44,7 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}

@ -16,11 +16,7 @@ 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') {
const init = build_expression(
{ ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await);
@ -51,8 +47,7 @@ export function ConstTag(node, context) {
const child_state = /** @type {ComponentContext['state']} */ ({
...context.state,
transform,
in_derived: true
transform
});
// TODO optimise the simple `{ x } = y` case — we can just return `y`

@ -11,7 +11,7 @@ import {
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
import { create_attribute, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -72,6 +72,7 @@ export function RegularElement(node, context) {
let has_spread = node.metadata.has_spread;
let has_use = false;
let should_remove_defaults = false;
for (const attribute of node.attributes) {
switch (attribute.type) {
@ -172,9 +173,14 @@ export function RegularElement(node, context) {
bindings.has('group') ||
(!bindings.has('group') && has_value_attribute))
) {
if (has_spread) {
// remove_input_defaults will be called inside set_attributes
should_remove_defaults = true;
} else {
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
}
}
}
if (node.name === 'textarea') {
const attribute = lookup.get('value') ?? lookup.get('checked');
@ -202,7 +208,15 @@ export function RegularElement(node, context) {
bindings.has('checked');
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
build_attribute_effect(
attributes,
class_directives,
style_directives,
context,
node,
node_id,
should_remove_defaults
);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
@ -392,6 +406,19 @@ export function RegularElement(node, context) {
}
if (!has_spread && needs_special_value_handling) {
if (node.metadata.synthetic_value_node) {
const synthetic_node = node.metadata.synthetic_value_node;
const synthetic_attribute = create_attribute(
'value',
synthetic_node.start,
synthetic_node.end,
[synthetic_node]
);
// this node is an `option` that didn't have a `value` attribute, but had
// a single-expression child, so we treat the value of that expression as
// the value of the option
build_element_special_value_attribute(node.name, node_id, synthetic_attribute, context, true);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
@ -399,6 +426,7 @@ export function RegularElement(node, context) {
}
}
}
}
context.state.template.pop_element();
}
@ -631,8 +659,15 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
* @param {boolean} [synthetic] - true if this should not sync to the DOM
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
function build_element_special_value_attribute(
element,
node_id,
attribute,
context,
synthetic = false
) {
const state = context.state;
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
@ -646,7 +681,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment(
const set_value_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
@ -655,14 +690,16 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
const update = b.stmt(
is_select_with_value
? b.sequence([
inner_assignment,
set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: inner_assignment
: synthetic
? assignment
: set_value_assignment
);
if (has_state) {

@ -12,8 +12,19 @@ export function TitleElement(node, context) {
/** @type {any} */ (node.fragment.nodes),
context
);
const evaluated = context.state.scope.evaluate(value);
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));
const statement = b.stmt(
b.assignment(
'=',
b.id('$.document.title'),
evaluated.is_known
? b.literal(evaluated.value)
: evaluated.is_defined
? value
: b.logical('??', value, b.literal(''))
)
);
if (has_state) {
context.state.update.push(statement);

@ -2,7 +2,7 @@
/** @import { Binding } from '#compiler' */
/** @import { ComponentContext, ParallelizedChunk } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths, is_expression_async } from '../../../../utils/ast.js';
import { extract_paths, is_expression_async, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
@ -336,12 +336,7 @@ export function VariableDeclaration(node, context) {
const bindings = [];
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
@ -350,8 +345,7 @@ export function VariableDeclaration(node, context) {
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
if (!parallelize) call = b.call(b.await(b.call('$.save', call)));
if (!parallelize) call = save(call);
if (dev) {
call = b.call(
'$.tag' + (parallelize ? '_async' : ''),
@ -370,12 +364,7 @@ export function VariableDeclaration(node, context) {
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
let rhs = value;
@ -392,7 +381,7 @@ export function VariableDeclaration(node, context) {
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
call = save(call);
}
if (dev) {

@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call

@ -16,6 +16,7 @@ import { build_expression, build_template_chunk, Memoizer } from './utils.js';
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {boolean} [should_remove_defaults]
*/
export function build_attribute_effect(
attributes,
@ -23,7 +24,8 @@ export function build_attribute_effect(
style_directives,
context,
element,
element_id
element_id,
should_remove_defaults = false
) {
/** @type {ObjectExpression['properties']} */
const values = [];
@ -91,6 +93,7 @@ export function build_attribute_effect(
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
should_remove_defaults && b.true,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
)

@ -6,7 +6,7 @@ import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js';
import * as b from '#compiler/builders';
import { dev, filename } from '../../../state.js';
import { component_name, dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -40,6 +40,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@ -187,23 +188,21 @@ export function server_component(analysis, options) {
template.body = [
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.let('$$inner_renderer'),
b.function_declaration(
b.id('$$render_inner'),
[b.id('$$payload')],
[b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
b.block([
b.stmt(b.assignment('=', b.id('$$settled'), b.true)),
b.stmt(
b.assignment('=', b.id('$$inner_payload'), b.call('$.copy_payload', b.id('$$payload')))
),
b.stmt(b.call('$$render_inner', b.id('$$inner_payload')))
b.stmt(b.assignment('=', b.id('$$inner_renderer'), b.call('$$renderer.copy'))),
b.stmt(b.call('$$render_inner', b.id('$$inner_renderer')))
])
),
b.stmt(b.call('$.assign_payload', b.id('$$payload'), b.id('$$inner_payload')))
b.stmt(b.call('$$renderer.subsume', b.id('$$inner_renderer')))
];
}
@ -239,26 +238,31 @@ export function server_component(analysis, options) {
template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props))));
}
const component_block = b.block([
let component_block = b.block([
.../** @type {Statement[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body)
]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments
component_block.loc = instance.loc;
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(
b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
b.const(analysis.props_id, b.call('$.props_id', b.id('$$renderer')))
);
}
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', dev && b.id(analysis.name))));
component_block.body.push(b.stmt(b.call('$.pop')));
component_block = b.block([
call_component_renderer(component_block, dev && b.id(component_name))
]);
}
if (analysis.uses_rest_props) {
@ -297,7 +301,7 @@ export function server_component(analysis, options) {
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);
body.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));
component_block.body.unshift(b.stmt(b.call('$$payload.css.add', b.id('$$css'))));
component_block.body.unshift(b.stmt(b.call('$$renderer.global.css.add', b.id('$$css'))));
}
let should_inject_props =
@ -311,7 +315,7 @@ export function server_component(analysis, options) {
const component_function = b.function_declaration(
b.id(analysis.name),
should_inject_props ? [b.id('$$payload'), b.id('$$props')] : [b.id('$$payload')],
should_inject_props ? [b.id('$$renderer'), b.id('$$props')] : [b.id('$$renderer')],
component_block
);
@ -377,6 +381,10 @@ export function server_component(analysis, options) {
);
}
if (options.experimental.async) {
body.unshift(b.imports([], 'svelte/internal/flags/async'));
}
return {
type: 'Program',
sourceType: 'module',

@ -1,19 +1,19 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close } from './shared/utils.js';
import { block_close, create_async_block } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push(
b.stmt(
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.await',
b.id('$$payload'),
b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
@ -23,7 +23,11 @@ export function AwaitBlock(node, context) {
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
)
)
),
block_close
);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);
}

@ -1,25 +1,40 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { save } from '../../../../utils/ast.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`,
// allow it, otherwise error
const argument = /** @type {Expression} */ (context.visit(node.argument));
if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
}
// we also need to restore context after block expressions
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
context.state.scope.function_depth === 0 ||
context.path.some(
(node) =>
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression'
)
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return context.next();
break;
}
// @ts-ignore
if (parent.metadata) {
if (parent.type !== 'ExpressionTag' && parent.type !== 'Fragment') {
return save(argument);
}
break;
}
}
return b.call('$.await_outside_boundary');
return argument === node.argument ? node : { ...node, argument };
}

@ -1,9 +1,8 @@
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -18,7 +17,9 @@ export function EachBlock(node, context) {
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const array_id = state.scope.root.unique('each_array');
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement} */
let block = b.block([b.const(array_id, b.call('$.ensure_array_like', collection))]);
/** @type {Statement[]} */
const each = [];
@ -44,23 +45,27 @@ export function EachBlock(node, context) {
);
if (node.fallback) {
const open = b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open));
const open = b.stmt(b.call(b.id('$$renderer.push'), block_open));
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
state.template.push(
block.body.push(
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
fallback
),
block_close
)
);
} else {
state.template.push(block_open, for_loop, block_close);
state.template.push(block_open);
block.body.push(for_loop);
}
if (node.metadata.expression.has_await) {
state.template.push(create_async_block(block), block_close);
} else {
state.template.push(...block.body, block_close);
}
}

@ -9,5 +9,10 @@ import * as b from '#compiler/builders';
*/
export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
context.state.template.push(b.call('$.html', expression));
const call = b.call('$.html', expression);
context.state.template.push(
node.metadata.expression.has_await
? b.stmt(b.call('$$renderer.push', b.thunk(call, true)))
: call
);
}

@ -1,9 +1,8 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -17,13 +16,16 @@ export function IfBlock(node, context) {
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open))
);
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
alternate.body.unshift(
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
context.state.template.push(b.if(test, consequent, alternate), block_close);
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (node.metadata.expression.has_await) {
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);
}

@ -7,8 +7,13 @@ import { is_void } from '../../../../../utils.js';
import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.js';
import { process_children, build_template, build_attribute_value } from './shared/utils.js';
import { build_element_attributes, prepare_element_spread_object } from './shared/element.js';
import {
process_children,
build_template,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/**
* @param {AST.RegularElement} node
@ -22,21 +27,56 @@ export function RegularElement(node, context) {
...context.state,
namespace,
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
};
const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
const optimiser = new PromiseOptimiser();
// If this element needs special handling (like <select value> / <option>),
// avoid calling build_element_attributes here to prevent evaluating/awaiting
// attribute expressions twice. We'll handle attributes in the special branch.
const is_select_special =
node.name === 'select' &&
node.attributes.some(
(attribute) =>
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
);
const is_option_special = node.name === 'option';
const is_special = is_select_special || is_option_special;
let body = /** @type {Expression | null} */ (null);
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${node.name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
context.state.template.push(
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
);
// TODO this is a real edge case, would be good to DRY this out
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
return;
}
@ -63,7 +103,7 @@ export function RegularElement(node, context) {
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
b.id('$$renderer'),
b.literal(node.name),
b.literal(location.line),
b.literal(location.column)
@ -72,84 +112,64 @@ export function RegularElement(node, context) {
);
}
let select_with_value = false;
if (is_select_special) {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
if (node.name === 'select') {
const value = node.attributes.find(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value'
const fn = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
if (node.attributes.some((attribute) => attribute.type === 'SpreadAttribute')) {
select_with_value = true;
state.template.push(
b.stmt(
b.assignment(
'=',
b.id('$$payload.select_value'),
b.member(
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
)
);
} else if (value) {
select_with_value = true;
const left = b.id('$$payload.select_value');
if (value.type === 'Attribute') {
state.template.push(
b.stmt(b.assignment('=', left, build_attribute_value(value.value, context)))
);
} else if (value.type === 'BindDirective') {
state.template.push(
b.stmt(
b.assignment(
'=',
left,
value.expression.type === 'SequenceExpression'
? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(value.expression))
)
)
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
}
return;
}
if (
node.name === 'option' &&
!node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
)
) {
if (is_option_special) {
let body;
if (node.metadata.synthetic_value_node) {
body = optimiser.transform(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression
);
} else {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$payload'),
b.thunk(b.block([...inner_state.init, ...build_template(inner_state.template)]))
)
)
body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
} else if (body !== null) {
}
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
@ -174,10 +194,6 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state });
}
if (select_with_value) {
state.template.push(b.stmt(b.assignment('=', b.id('$$payload.select_value'), b.void0)));
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}
@ -185,4 +201,16 @@ export function RegularElement(node, context) {
if (dev) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}

@ -23,7 +23,7 @@ export function RenderTag(node, context) {
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$payload'),
b.id('$$renderer'),
...snippet_args
)
)

@ -2,7 +2,13 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { empty_comment, build_attribute_value } from './shared/utils.js';
import {
build_attribute_value,
PromiseOptimiser,
create_async_block,
block_open,
block_close
} from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@ -15,13 +21,22 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */
const spreads = [];
const optimiser = new PromiseOptimiser();
let name = b.literal('default');
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(/** @type {Expression} */ (context.visit(attribute)));
let expression = /** @type {Expression} */ (context.visit(attribute));
spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
const value = build_attribute_value(attribute.value, context, false, true);
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name === 'name') {
name = /** @type {Literal} */ (value);
@ -43,12 +58,17 @@ export function SlotElement(node, context) {
const slot = b.call(
'$.slot',
b.id('$$payload'),
b.id('$$renderer'),
b.id('$$props'),
name,
props_expression,
fallback
);
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
const statement =
optimiser.expressions.length > 0
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
context.state.template.push(block_open, statement, block_close);
}

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';
/**
* @param {AST.SnippetBlock} node
@ -11,17 +12,21 @@ import * as b from '#compiler/builders';
export function SnippetBlock(node, context) {
let fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
[b.id('$$renderer'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
if (node.body.metadata.has_await) {
fn.body = b.block([create_async_block(fn.body)]);
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
if (dev) {
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$renderer'))));
statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
}

@ -1,21 +1,30 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/utils.js';
import {
block_close,
block_open,
block_open_else,
build_attribute_value,
build_template,
create_async_block
} from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
);
const is_pending_attr_nullish =
pending_attribute &&
typeof pending_attribute.value === 'object' &&
!Array.isArray(pending_attribute.value) &&
!context.state.scope.evaluate(pending_attribute.value.expression).is_defined;
const pending_snippet = /** @type {AST.SnippetBlock} */ (
node.fragment.nodes.find(
@ -23,16 +32,44 @@ export function SvelteBoundary(node, context) {
)
);
if (pending_attribute) {
const value = build_attribute_value(pending_attribute.value, context, false, true);
context.state.template.push(b.call(value, b.id('$$payload')));
} else if (pending_snippet) {
if (pending_attribute || pending_snippet) {
if (pending_attribute && is_pending_attr_nullish && !pending_snippet) {
const callee = build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
);
const pending = b.call(callee, b.id('$$renderer'));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
/** @type {BlockStatement} */ (context.visit(pending_snippet.body))
b.if(
callee,
b.block(build_template([block_open_else, pending, block_close])),
b.block(build_template([block_open, block, block_close]))
)
);
} else {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const pending = pending_attribute
? b.call(
build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
),
b.id('$$renderer')
)
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body));
context.state.template.push(block_open_else, pending, block_close);
}
} else {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(block_open, statement, block_close);
}
context.state.template.push(b.literal(BLOCK_CLOSE));
}

@ -1,12 +1,12 @@
/** @import { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template } from './shared/utils.js';
import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -37,7 +37,9 @@ export function SvelteElement(node, context) {
init: []
};
build_element_attributes(node, { ...context, state });
const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
@ -45,7 +47,7 @@ export function SvelteElement(node, context) {
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
b.id('$$renderer'),
tag,
b.literal(location.line),
b.literal(location.column)
@ -57,18 +59,23 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push(
b.stmt(
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.element',
b.id('$$payload'),
b.id('$$renderer'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
)
);
if (optimiser.expressions.length > 0) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
}

@ -11,6 +11,6 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block)))
b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block)))
);
}

@ -13,5 +13,9 @@ export function TitleElement(node, context) {
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>'));
context.state.init.push(...build_template(template, b.id('$$payload.title'), '='));
context.state.init.push(
b.stmt(
b.call('$$renderer.title', b.arrow([b.id('$$renderer')], b.block(build_template(template))))
)
);
}

@ -1,7 +1,13 @@
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js';
import {
empty_comment,
build_attribute_value,
create_async_block,
PromiseOptimiser,
build_template
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.js';
@ -72,16 +78,26 @@ export function build_inline_component(node, expression, context) {
}
}
const optimiser = new PromiseOptimiser();
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
lets.default.push(attribute);
}
} else if (attribute.type === 'SpreadAttribute') {
props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute)));
let expression = /** @type {Expression} */ (context.visit(attribute));
props_and_spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name.startsWith('--')) {
const value = build_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value));
continue;
}
@ -90,7 +106,6 @@ export function build_inline_component(node, expression, context) {
has_children_prop = true;
}
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SequenceExpression') {
@ -201,7 +216,7 @@ export function build_inline_component(node, expression, context) {
if (block.body.length === 0) continue;
/** @type {Pattern[]} */
const params = [b.id('$$payload')];
const params = [b.id('$$renderer')];
if (lets[slot_name].length > 0) {
const pattern = b.object_pattern(
@ -227,7 +242,12 @@ export function build_inline_component(node, expression, context) {
params.push(pattern);
}
const slot_fn = b.arrow(params, b.block(block.body));
const slot_fn = b.arrow(
params,
node.fragment.metadata.has_await
? b.block([create_async_block(b.block(block.body))])
: b.block(block.body)
);
if (slot_name === 'default' && !has_children_prop) {
if (
@ -278,7 +298,7 @@ export function build_inline_component(node, expression, context) {
let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
expression,
b.id('$$payload'),
b.id('$$renderer'),
props_expression
)
);
@ -291,27 +311,33 @@ export function build_inline_component(node, expression, context) {
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) {
context.state.template.push(
b.stmt(
statement = b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.id('$$renderer'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement])),
dynamic && b.true
)
)
);
} else {
if (dynamic) {
}
if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
}
if (dynamic && custom_css_props.length === 0) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries) {
if (
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0
) {
context.state.template.push(empty_comment);
}
}
}

@ -1,5 +1,5 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
@ -11,6 +11,7 @@ import {
import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '#compiler/builders';
import {
ELEMENT_IS_INPUT,
ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE
} from '../../../../../../constants.js';
@ -29,8 +30,9 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
* their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
export function build_element_attributes(node, context) {
export function build_element_attributes(node, context, transform) {
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -61,7 +63,8 @@ export function build_element_attributes(node, context) {
// also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data;
}
content = b.call('$.escape', build_attribute_value(attribute.value, context));
content = b.call('$.escape', build_attribute_value(attribute.value, context, transform));
} else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no
// effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute)
@ -149,12 +152,12 @@ export function build_element_attributes(node, context) {
expression: is_checkbox
? b.call(
b.member(attribute.expression, 'includes'),
build_attribute_value(value_attribute.value, context)
build_attribute_value(value_attribute.value, context, transform)
)
: b.binary(
'===',
attribute.expression,
build_attribute_value(value_attribute.value, context)
build_attribute_value(value_attribute.value, context, transform)
),
metadata: {
expression: create_expression_metadata()
@ -201,30 +204,14 @@ export function build_element_attributes(node, context) {
}
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
if (node.name === 'option') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
b.member(
build_spread_object(
build_element_spread_attributes(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
attributes,
style_directives,
class_directives,
context,
transform
);
}
} else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
@ -239,6 +226,7 @@ export function build_element_attributes(node, context) {
build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
)
).value;
@ -259,22 +247,13 @@ export function build_element_attributes(node, context) {
);
}
if (node.name === 'option' && name === 'value') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
literal_value != null ? b.literal(/** @type {any} */ (literal_value)) : b.void0
)
);
}
continue;
}
const value = build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
@ -285,18 +264,16 @@ export function build_element_attributes(node, context) {
}
context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') {
context.state.template.push(build_attr_class(class_directives, value, context, css_hash));
context.state.template.push(
build_attr_class(class_directives, value, context, css_hash, transform)
);
} else if (name === 'style') {
context.state.template.push(build_attr_style(style_directives, value, context));
context.state.template.push(build_attr_style(style_directives, value, context, transform));
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
if (name === 'value' && node.name === 'option') {
context.state.template.push(b.call('$.maybe_selected', b.id('$$payload'), value));
}
}
}
@ -327,17 +304,20 @@ function get_attribute_name(element, attribute) {
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
export function build_spread_object(element, attributes, context) {
return b.object(
export function build_spread_object(element, attributes, context, transform) {
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute);
const value = build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute);
@ -345,12 +325,20 @@ export function build_spread_object(element, attributes, context) {
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
return b.spread(
transform(
/** @type {Expression} */ (context.visit(attribute)),
attribute.metadata.expression
)
);
})
);
return object;
}
/**
@ -360,15 +348,90 @@ export function build_spread_object(element, attributes, context) {
* @param {AST.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_element_spread_attributes(
element,
attributes,
style_directives,
class_directives,
context
context,
transform
) {
const args = prepare_element_spread(
element,
/** @type {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */ (attributes),
style_directives,
class_directives,
context,
transform
);
let call = b.call('$.attributes', ...args);
context.state.template.push(call);
}
/**
* Prepare args for $.attributes(...): compute object, css_hash, classes, styles and flags.
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @returns {[ObjectExpression,Literal | undefined, ObjectExpression | undefined, ObjectExpression | undefined, Literal | undefined]}
*/
export function prepare_element_spread_object(element, context, transform) {
/** @type {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */
const select_attributes = [];
/** @type {AST.ClassDirective[]} */
const class_directives = [];
/** @type {AST.StyleDirective[]} */
const style_directives = [];
for (const attribute of element.attributes) {
if (
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
) {
select_attributes.push(attribute);
} else if (attribute.type === 'ClassDirective') {
class_directives.push(attribute);
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
}
}
return prepare_element_spread(
element,
select_attributes,
style_directives,
class_directives,
context,
transform
);
}
/**
* Prepare args for $.attributes(...): compute object, css_hash, classes, styles and flags.
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {AST.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @returns {[ObjectExpression,Literal | undefined, ObjectExpression | undefined, ObjectExpression | undefined, Literal | undefined]}
*/
export function prepare_element_spread(
element,
attributes,
style_directives,
class_directives,
context,
transform
) {
/** @type {ObjectExpression | undefined} */
let classes;
/** @type {ObjectExpression | undefined} */
let styles;
let flags = 0;
@ -378,9 +441,13 @@ function build_element_spread_attributes(
directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression))
: transform(
/** @type {Expression} */ (context.visit(directive.expression)),
directive.metadata.expression
)
)
);
classes = b.object(properties);
}
@ -390,10 +457,9 @@ function build_element_spread_attributes(
directive.name,
directive.value === true
? b.id(directive.name)
: build_attribute_value(directive.value, context, true)
: build_attribute_value(directive.value, context, transform, true)
)
);
styles = b.object(properties);
}
@ -401,17 +467,17 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
} else if (is_custom_element_node(element)) {
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
} else if (element.type === 'RegularElement' && element.name === 'input') {
flags |= ELEMENT_IS_INPUT;
}
const object = build_spread_object(element, attributes, context);
const object = build_spread_object(element, attributes, context, transform);
const css_hash =
element.metadata.scoped && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: b.null;
: undefined;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
return [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
}
/**
@ -420,8 +486,9 @@ function build_element_spread_attributes(
* @param {Expression} expression
* @param {ComponentContext} context
* @param {string | null} hash
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_attr_class(class_directives, expression, context, hash) {
function build_attr_class(class_directives, expression, context, hash, transform) {
/** @type {ObjectExpression | undefined} */
let directives;
@ -431,7 +498,10 @@ function build_attr_class(class_directives, expression, context, hash) {
b.prop(
'init',
b.literal(directive.name),
/** @type {Expression} */ (context.visit(directive.expression, context.state))
transform(
/** @type {Expression} */ (context.visit(directive.expression, context.state)),
directive.metadata.expression
)
)
)
);
@ -454,9 +524,10 @@ function build_attr_class(class_directives, expression, context, hash) {
*
* @param {AST.StyleDirective[]} style_directives
* @param {Expression} expression
* @param {ComponentContext} context
* @param {ComponentContext} context,
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
function build_attr_style(style_directives, expression, context) {
function build_attr_style(style_directives, expression, context, transform) {
/** @type {ArrayExpression | ObjectExpression | undefined} */
let directives;
@ -468,7 +539,7 @@ function build_attr_style(style_directives, expression, context) {
const expression =
directive.value === true
? b.id(directive.name)
: build_attribute_value(directive.value, context, true);
: build_attribute_value(directive.value, context, transform, true);
let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') {

@ -1,20 +1,25 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
import {
BLOCK_CLOSE,
BLOCK_OPEN,
BLOCK_OPEN_ELSE,
EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
import { has_await } from '../../../../../utils/ast.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN);
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open_else = b.literal(BLOCK_OPEN_ELSE);
/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
export const block_close = b.literal(BLOCK_CLOSE);
@ -32,6 +37,10 @@ export function process_children(nodes, { visit, state }) {
let sequence = [];
function flush() {
if (sequence.length === 0) {
return;
}
let quasi = b.quasi('', false);
const quasis = [quasi];
@ -63,26 +72,25 @@ export function process_children(nodes, { visit, state }) {
}
state.template.push(b.template(quasis, expressions));
sequence = [];
}
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
for (const node of nodes) {
if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
flush();
const visited = /** @type {Expression} */ (visit(node.expression));
state.template.push(
b.stmt(b.call('$$renderer.push', b.thunk(b.call('$.escape', visited), true)))
);
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
if (sequence.length > 0) {
flush();
sequence = [];
}
visit(node, { ...state });
}
}
if (sequence.length > 0) {
flush();
}
}
/**
@ -95,11 +103,9 @@ function is_statement(node) {
/**
* @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]}
*/
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') {
export function build_template(template) {
/** @type {string[]} */
let strings = [];
@ -110,11 +116,10 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = [];
const flush = () => {
if (operator === 'push') {
statements.push(
b.stmt(
b.call(
b.member(out, b.id('push')),
b.id('$$renderer.push'),
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
@ -122,20 +127,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
)
)
);
} else {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
)
);
}
strings = [];
expressions = [];
};
@ -178,6 +170,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
*
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @param {boolean} trim_whitespace
* @param {boolean} is_component
* @returns {Expression}
@ -185,6 +178,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
export function build_attribute_value(
value,
context,
transform,
trim_whitespace = false,
is_component = false
) {
@ -203,7 +197,10 @@ export function build_attribute_value(
return b.literal(is_component ? data : escape_html(data, true));
}
return /** @type {Expression} */ (context.visit(chunk.expression));
return transform(
/** @type {Expression} */ (context.visit(chunk.expression)),
chunk.metadata.expression
);
}
let quasi = b.quasi('', false);
@ -221,7 +218,13 @@ export function build_attribute_value(
: node.data;
} else {
expressions.push(
b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression)))
b.call(
'$.stringify',
transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
)
)
);
quasi = b.quasi('', i + 1 === value.length);
@ -257,3 +260,70 @@ export function build_getter(node, state) {
return node;
}
/**
* Creates a `$$renderer.child(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {boolean} async
* @returns {Statement}
*/
export function create_child_block(body, async) {
return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, async)));
}
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
}
/**
* @param {BlockStatement | Expression} body
* @param {Identifier | false} component_fn_id
* @returns {Statement}
*/
export function call_component_renderer(body, component_fn_id) {
return b.stmt(
b.call('$$renderer.component', b.arrow([b.id('$$renderer')], body, false), component_fn_id)
);
}
export class PromiseOptimiser {
/** @type {Expression[]} */
expressions = [];
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
if (metadata.has_await) {
const length = this.expressions.push(expression);
return b.id(`$$${length - 1}`);
}
return expression;
};
apply() {
if (this.expressions.length === 1) {
return b.const('$$0', this.expressions[0]);
}
const promises = b.array(
this.expressions.map((expression) => {
return expression.type === 'AwaitExpression' && !has_await(expression.argument)
? expression.argument
: b.call(b.thunk(expression, true));
})
);
return b.const(
b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
b.await(b.call('Promise.all', promises))
);
}
}

@ -260,6 +260,12 @@ class Evaluation {
break;
}
if (binding.initial?.type === 'SnippetBlock') {
this.is_defined = true;
this.is_known = false;
break;
}
if (!binding.updated && binding.initial !== null && !is_prop) {
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
break;
@ -1032,7 +1038,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
Component: (node, context) => {
context.state.scope.reference(b.id(node.name), context.path);
context.state.scope.reference(b.id(node.name.split('.')[0]), context.path);
Component(node, context);
},
SvelteSelf: Component,

@ -1,10 +1,10 @@
import type { AST, Binding, StateField } from '#compiler';
import type {
AwaitExpression,
CallExpression,
ClassBody,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
@ -47,6 +47,8 @@ export interface Analysis {
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
/** Awaits needing context preservation */
pickled_awaits: Set<AwaitExpression>;
}
export interface ComponentAnalysis extends Analysis {

@ -105,7 +105,7 @@ export interface CompileOptions extends ModuleCompileOptions {
css?: 'injected' | 'external';
/**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
* It defaults to returning `svelte-${hash(css)}`.
* It defaults to returning `svelte-${hash(filename ?? css)}`.
*
* @default undefined
*/
@ -283,11 +283,16 @@ export type DeclarationKind =
| 'var'
| 'let'
| 'const'
| 'using'
| 'await using'
| 'function'
| 'import'
| 'param'
| 'rest_param'
| 'synthetic';
| 'synthetic'
// TODO not yet implemented, but needed for TypeScript reasons
| 'using'
| 'await using';
export interface ExpressionMetadata {
/** All the bindings that are referenced eagerly (not inside functions) in this expression */

@ -344,6 +344,8 @@ export namespace AST {
has_spread: boolean;
scoped: boolean;
path: SvelteNode[];
/** Synthetic value attribute for <option> with single expression child, used for client-only handling */
synthetic_value_node: ExpressionTag | null;
};
}

@ -609,3 +609,27 @@ export function build_assignment_value(operator, left, right) {
? b.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
: b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}
/**
* @param {ESTree.Expression} expression
*/
export function has_await(expression) {
let has_await = false;
walk(expression, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
return has_await;
}
/**
* Turns `await ...` to `(await $.save(...))()`
* @param {ESTree.Expression} expression
*/
export function save(expression) {
return b.call(b.await(b.call('$.save', expression)));
}

@ -2,6 +2,7 @@
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
import { has_await } from './ast.js';
/**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -363,7 +364,14 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition}
*/
export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static };
return {
type: 'PropertyDefinition',
decorators: [],
key,
value,
computed,
static: is_static
};
}
/**
@ -443,16 +451,7 @@ export function thunk(expression, async = false) {
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
let has_await = false;
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
if (!has_await(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
@ -573,6 +572,7 @@ function for_builder(init, test, update, body) {
export function method(kind, key, params, body, computed = false, is_static = false) {
return {
type: 'MethodDefinition',
decorators: [],
key,
kind,
value: function_builder(null, params, block(body)),
@ -618,6 +618,7 @@ function if_builder(test, consequent, alternate) {
export function import_all(as, source) {
return {
type: 'ImportDeclaration',
attributes: [],
source: literal(source),
specifiers: [import_namespace(as)]
};
@ -631,6 +632,7 @@ export function import_all(as, source) {
export function imports(parts, source) {
return {
type: 'ImportDeclaration',
attributes: [],
source: literal(source),
specifiers: parts.map((p) => ({
type: 'ImportSpecifier',

@ -70,8 +70,8 @@ const component_options = {
return input;
}),
cssHash: fun(({ css, hash }) => {
return `svelte-${hash(css)}`;
cssHash: fun(({ css, filename, hash }) => {
return `svelte-${hash(filename === '(unknown)' ? css : filename ?? css)}`;
}),
// TODO this is a sourcemap option, would be good to put under a sourcemap namespace

@ -174,11 +174,11 @@ export function a11y_click_events_have_key_events(node) {
}
/**
* Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
* Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
* @param {null | NodeLike} node
*/
export function a11y_consider_explicit_label(node) {
w(node, 'a11y_consider_explicit_label', `Buttons and links should either contain text or have an \`aria-label\` or \`aria-labelledby\` attribute\nhttps://svelte.dev/e/a11y_consider_explicit_label`);
w(node, 'a11y_consider_explicit_label', `Buttons and links should either contain text or have an \`aria-label\`, \`aria-labelledby\` or \`title\` attribute\nhttps://svelte.dev/e/a11y_consider_explicit_label`);
}
/**

@ -28,6 +28,7 @@ export const HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1;
export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1;
export const ELEMENT_IS_INPUT = 1 << 2;
export const UNINITIALIZED = Symbol();

@ -1,12 +1,12 @@
/** @import { Component } from '#server' */
import { current_component } from './internal/server/context.js';
/** @import { SSRContext } from '#server' */
/** @import { Renderer } from './internal/server/renderer.js' */
import { ssr_context } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js';
import * as e from './internal/server/errors.js';
/** @param {() => void} fn */
export function onDestroy(fn) {
var context = /** @type {Component} */ (current_component);
(context.d ??= []).push(fn);
/** @type {Renderer} */ (/** @type {SSRContext} */ (ssr_context).r).on_destroy(fn);
}
export {

@ -1,7 +1,15 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import { get_pending_boundary } from './boundary.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
@ -9,11 +17,26 @@ import { get_pending_boundary } from './boundary.js';
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, expressions, fn) {
var boundary = get_pending_boundary();
var boundary = get_boundary();
boundary.update_pending_count(1);
var was_hydrating = hydrating;
if (was_hydrating) {
hydrate_next();
var previous_hydrate_node = hydrate_node;
var end = skip_nodes(false);
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
@ -22,5 +45,9 @@ export function async(node, expressions, fn) {
} finally {
boundary.update_pending_count(-1);
}
if (was_hydrating) {
set_hydrating(false);
}
});
}

@ -8,7 +8,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -22,7 +22,7 @@ import {
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
import { flushSync } from '../../reactivity/batch.js';
import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
const PENDING = 0;
const THEN = 1;
@ -126,7 +126,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flushSync();
if (!is_flushing_sync) flushSync();
}
}
}
@ -140,7 +140,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -1,5 +1,11 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import {
BOUNDARY_EFFECT,
COMMENT_NODE,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
} from '#client/constants';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -15,7 +21,7 @@ import {
hydrate_node,
hydrating,
next,
remove_nodes,
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
@ -23,7 +29,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { Batch, current_batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -49,16 +55,16 @@ export function boundary(node, props, children) {
}
export class Boundary {
pending = false;
/** @type {Boundary | null} */
parent;
#pending = false;
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {TemplateNode | null} */
#hydrate_open = hydrating ? hydrate_node : null;
/** @type {BoundaryProps} */
#props;
@ -81,7 +87,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
#local_pending_count = 0;
#pending_count = 0;
#is_creating_fallback = false;
/**
@ -95,12 +103,12 @@ export class Boundary {
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
internal_set(this.#effect_pending, this.#local_pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count);
this.#effect_pending = source(this.#local_pending_count);
if (DEV) {
tag(this.#effect_pending, '$effect.pending()');
@ -121,28 +129,65 @@ export class Boundary {
this.#props = props;
this.#children = children;
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b;
this.pending = !!this.#props.pending;
this.#pending = !!this.#props.pending;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
const comment = this.#hydrate_open;
hydrate_next();
const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
/** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
if (server_rendered_pending) {
this.#hydrate_pending_content();
} else {
this.#hydrate_resolved_content();
}
} else {
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
const pending = this.#props.pending;
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.#pending = false;
}
}
}, flags);
if (hydrating) {
this.#anchor = hydrate_node;
}
}
#hydrate_resolved_content() {
try {
this.#main_effect = branch(() => this.#children(this.#anchor));
} catch (error) {
this.error(error);
}
if (hydrating && pending) {
// Since server rendered resolved content, we never show pending state
// Even if client-side async operations are still running, the content is already displayed
this.#pending = false;
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) {
return;
}
this.#pending_effect = branch(() => pending(this.#anchor));
// future work: when we have some form of async SSR, we will
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
@ -156,27 +201,17 @@ export class Boundary {
this.#pending_effect = null;
});
this.pending = false;
this.#pending = false;
}
});
} else {
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
}
}
}, flags);
if (hydrating) {
this.#anchor = hydrate_node;
}
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
*/
is_pending() {
return this.#pending || (!!this.parent && this.parent.is_pending());
}
has_pending_snippet() {
@ -220,12 +255,25 @@ export class Boundary {
}
}
/** @param {1 | -1} d */
/**
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
* @param {1 | -1} d
*/
#update_pending_count(d) {
if (!this.has_pending_snippet()) {
if (this.parent) {
this.parent.#update_pending_count(d);
}
// if there's no parent, we're in a scope with no pending snippet
return;
}
this.#pending_count += d;
if (this.#pending_count === 0) {
this.pending = false;
this.#pending = false;
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
@ -240,14 +288,16 @@ export class Boundary {
}
}
/** @param {1 | -1} d */
/**
* Update the source that powers `$effect.pending()` inside this boundary,
* and controls when the current `pending` snippet (if any) is removed.
* Do not call from inside the class
* @param {1 | -1} d
*/
update_pending_count(d) {
if (this.has_pending_snippet()) {
this.#update_pending_count(d);
} else if (this.parent) {
this.parent.#update_pending_count(d);
}
this.#local_pending_count += d;
effect_pending_updates.add(this.#effect_pending_update);
}
@ -261,6 +311,12 @@ export class Boundary {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
@ -277,9 +333,9 @@ export class Boundary {
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next();
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
var did_reset = false;
@ -297,7 +353,10 @@ export class Boundary {
e.svelte_boundary_reset_onerror();
}
this.#pending_count = 0;
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#local_pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
@ -305,7 +364,9 @@ export class Boundary {
});
}
this.pending = true;
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.#pending = this.has_pending_snippet();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
@ -315,16 +376,10 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.pending = false;
this.#pending = false;
}
};
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
var previous_reaction = active_reaction;
try {
@ -381,18 +436,8 @@ function move_effect(effect, fragment) {
}
}
export function get_pending_boundary() {
var boundary = /** @type {Effect} */ (active_effect).b;
while (boundary !== null && !boundary.has_pending_snippet()) {
boundary = boundary.parent;
}
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
export function pending() {

@ -14,7 +14,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -209,7 +209,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
@ -259,7 +259,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// remove excess nodes
if (length > 0) {
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
}

@ -6,7 +6,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -93,7 +93,7 @@ export function if_block(node, fn, elseif = false) {
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -6,7 +6,7 @@ import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_idle_task } from '../task.js';
import { queue_micro_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import {
active_effect,
@ -65,7 +65,7 @@ export function remove_input_defaults(input) {
// @ts-expect-error
input.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
queue_micro_task(remove_defaults);
add_form_reset_listener();
}
@ -268,10 +268,27 @@ export function set_custom_element_data(node, prop, value) {
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
export function set_attributes(element, prev, next, css_hash, skip_warning = false) {
function set_attributes(
element,
prev,
next,
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
if (hydrating && should_remove_defaults && element.tagName === 'INPUT') {
var input = /** @type {HTMLInputElement} */ (element);
var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue';
if (!(attribute in next)) {
remove_input_defaults(input);
}
}
var attributes = get_attributes(element);
var is_custom_element = attributes[IS_CUSTOM_ELEMENT];
@ -467,6 +484,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
*/
export function attribute_effect(
@ -475,6 +493,7 @@ export function attribute_effect(
sync = [],
async = [],
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
flatten(sync, async, (values) => {
@ -490,7 +509,14 @@ export function attribute_effect(
block(() => {
var next = fn(...values.map(get));
/** @type {Record<string | symbol, any>} */
var current = set_attributes(element, prev, next, css_hash, skip_warning);
var current = set_attributes(
element,
prev,
next,
css_hash,
should_remove_defaults,
skip_warning
);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value);

@ -81,9 +81,10 @@ export function next(count = 1) {
}
/**
* Removes all nodes starting at `hydrate_node` up until the next hydration end comment
* Skips or removes (depending on {@link remove}) all nodes starting at `hydrate_node` up until the next hydration end comment
* @param {boolean} remove
*/
export function remove_nodes() {
export function skip_nodes(remove = true) {
var depth = 0;
var node = hydrate_node;
@ -100,7 +101,7 @@ export function remove_nodes() {
}
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
if (remove) node.remove();
node = next;
}
}

@ -130,11 +130,11 @@ export function child(node, is_text) {
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {DocumentFragment | TemplateNode[]} fragment
* @param {boolean} is_text
* @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment
* @param {boolean} [is_text]
* @returns {Node | null}
*/
export function first_child(fragment, is_text) {
export function first_child(fragment, is_text = false) {
if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));

@ -1,60 +1,42 @@
import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available
const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
import { is_flushing_sync } from '../reactivity/batch.js';
/** @type {Array<() => void>} */
let micro_tasks = [];
/** @type {Array<() => void>} */
let idle_tasks = [];
function run_micro_tasks() {
var tasks = micro_tasks;
micro_tasks = [];
run_all(tasks);
}
function run_idle_tasks() {
var tasks = idle_tasks;
idle_tasks = [];
run_all(tasks);
}
/**
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
if (micro_tasks.length === 0) {
queueMicrotask(run_micro_tasks);
if (micro_tasks.length === 0 && !is_flushing_sync) {
var tasks = micro_tasks;
queueMicrotask(() => {
// If this is false, a flushSync happened in the meantime. Do _not_ run new scheduled microtasks in that case
// as the ordering of microtasks would be broken at that point - consider this case:
// - queue_micro_task schedules microtask A to flush task X
// - synchronously after, flushSync runs, processing task X
// - synchronously after, some other microtask B is scheduled, but not through queue_micro_task but for example a Promise.resolve() in user code
// - synchronously after, queue_micro_task schedules microtask C to flush task Y
// - one tick later, microtask A now resolves, flushing task Y before microtask B, which is incorrect
// This if check prevents that race condition (that realistically will only happen in tests)
if (tasks === micro_tasks) run_micro_tasks();
});
}
micro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
if (idle_tasks.length === 0) {
request_idle_callback(run_idle_tasks);
}
idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (micro_tasks.length > 0) {
while (micro_tasks.length > 0) {
run_micro_tasks();
}
if (idle_tasks.length > 0) {
run_idle_tasks();
}
}

@ -316,6 +316,9 @@ export function text(value = '') {
return node;
}
/**
* @returns {TemplateNode | DocumentFragment}
*/
export function comment() {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
@ -362,7 +365,7 @@ export function props_id() {
hydrating &&
hydrate_node &&
hydrate_node.nodeType === COMMENT_NODE &&
hydrate_node.textContent?.startsWith(`#`)
hydrate_node.textContent?.startsWith(`$`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();

@ -28,7 +28,6 @@ export { attach } from './dom/elements/attachments.js';
export {
remove_input_defaults,
set_attribute,
set_attributes,
attribute_effect,
set_custom_element_data,
set_xlink_attribute,
@ -106,7 +105,7 @@ export {
save,
track_reactivity_loss
} from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export { flushSync as flush } from './reactivity/batch.js';
export {
async_derived,
user_derived as derived,

@ -3,7 +3,7 @@
import { DESTROYED } from '#client/constants';
import { DEV } from 'esm-env';
import { component_context, is_runes, set_component_context } from '../context.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
@ -11,7 +11,7 @@ import {
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { current_batch, suspend } from './batch.js';
import { Batch, current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
@ -20,6 +20,14 @@ import {
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
/**
*
@ -39,7 +47,8 @@ export function flatten(sync, async, fn) {
var parent = /** @type {Effect} */ (active_effect);
var restore = capture();
var boundary = get_pending_boundary();
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
@ -56,11 +65,15 @@ export function flatten(sync, async, fn) {
}
}
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate();
unset_context();
})
.catch((error) => {
boundary.error(error);
invoke_error_boundary(error, parent);
});
}
@ -75,12 +88,23 @@ function capture() {
var previous_component_context = component_context;
var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (DEV) {
set_from_async_derived(null);
}
@ -178,17 +202,52 @@ export function unset_context() {
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
var unsuspend = suspend();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
boundary.update_pending_count(1);
if (!pending) batch.increment();
var active = /** @type {Effect} */ (active_effect);
var was_hydrating = hydrating;
var next_hydrate_node = undefined;
if (was_hydrating) {
hydrate_next();
next_hydrate_node = skip_nodes(false);
}
try {
var promise = fn();
} finally {
if (next_hydrate_node) {
set_hydrate_node(next_hydrate_node);
hydrate_next();
}
}
try {
await fn();
await promise;
} catch (error) {
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
} finally {
unsuspend();
if (was_hydrating) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
if (pending) {
batch.flush();
} else {
batch.decrement();
}
unset_context();
}
}

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source } from '#client' */
/** @import { Derived, Effect, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@ -10,12 +10,11 @@ import {
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
USER_EFFECT,
MAYBE_DIRTY
MAYBE_DIRTY,
DERIVED
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { deferred, define_property, noop } from '../../shared/utils.js';
import {
active_effect,
is_dirty,
@ -25,12 +24,11 @@ import {
update_effect
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js';
import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js';
import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
/** @type {Set<Batch>} */
const batches = new Set();
@ -56,19 +54,6 @@ export let batch_deriveds = null;
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
/** @type {Array<() => void>} */
let tasks = [];
function dequeue() {
const task = /** @type {() => void} */ (tasks.shift());
if (tasks.length > 0) {
queueMicrotask(dequeue);
}
task();
}
/** @type {Effect[]} */
let queued_root_effects = [];
@ -76,7 +61,7 @@ let queued_root_effects = [];
let last_scheduled_effect = null;
let is_flushing = false;
let is_flushing_sync = false;
export let is_flushing_sync = false;
export class Batch {
/**
@ -113,22 +98,8 @@ export class Batch {
#deferred = null;
/**
* True if an async effect inside this batch resolved and
* its parent branch was already deleted
*/
#neutered = false;
/**
* Async effects (created inside `async_derived`) encountered during processing.
* These run after the rest of the batch has updated, since they should
* always have the latest values
* @type {Effect[]}
*/
#async_effects = [];
/**
* The same as `#async_effects`, but for effects inside a newly-created
* `<svelte:boundary>` these do not prevent the batch from committing
* Async effects inside a newly-created `<svelte:boundary>`
* these do not prevent the batch from committing
* @type {Effect[]}
*/
#boundary_async_effects = [];
@ -181,32 +152,7 @@ export class Batch {
previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null;
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (async_mode_flag && batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of this.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
for (const batch of batches) {
if (batch === this) continue;
for (const [source, previous] of batch.#previous) {
if (!current_values.has(source)) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous;
}
}
}
}
var revert = Batch.apply(this);
for (const root of root_effects) {
this.#traverse_effect_tree(root);
@ -214,7 +160,7 @@ export class Batch {
// if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) {
if (this.#pending === 0) {
this.#commit();
var render_effects = this.#render_effects;
@ -226,21 +172,12 @@ export class Batch {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = current_batch;
previous_batch = this;
current_batch = null;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
// Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`.
// That method expects `current_batch` to be set, and could run the loop again if effects result in new effects
// being scheduled but without writes happening in which case no new batch is created.
if (current_batch === null) {
current_batch = this;
} else {
batches.delete(this);
}
this.#deferred?.resolve();
} else {
this.#defer_effects(this.#render_effects);
@ -248,27 +185,12 @@ export class Batch {
this.#defer_effects(this.#block_effects);
}
if (current_values) {
for (const [source, { v, wv }] of current_values) {
// reset the source to the current value (unless
// it got a newer value as a result of effects running)
if (source.wv <= wv) {
source.v = v;
}
}
batch_deriveds = null;
}
for (const effect of this.#async_effects) {
update_effect(effect);
}
revert();
for (const effect of this.#boundary_async_effects) {
update_effect(effect);
}
this.#async_effects = [];
this.#boundary_async_effects = [];
}
@ -297,9 +219,8 @@ export class Batch {
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) {
this.#boundary_async_effects.push(effect);
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect);
@ -372,25 +293,17 @@ export class Batch {
}
}
neuter() {
this.#neutered = true;
}
flush() {
if (queued_root_effects.length > 0) {
this.activate();
flush_effects();
} else {
this.#commit();
}
if (current_batch !== this) {
// this can happen if a `flushSync` occurred during `flush_effects()`,
// which is permitted in legacy mode despite being a terrible idea
if (current_batch !== null && current_batch !== this) {
// this can happen if a new batch was created during `flush_effects()`
return;
}
if (this.#pending === 0) {
batches.delete(this);
} else if (this.#pending === 0) {
this.#commit();
}
this.deactivate();
@ -400,13 +313,59 @@ export class Batch {
* Append and remove branches to/from the DOM
*/
#commit() {
if (!this.#neutered) {
for (const fn of this.#callbacks) {
fn();
}
}
this.#callbacks.clear();
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// recent value for a given source
if (batches.size > 1) {
this.#previous.clear();
let is_earlier = true;
for (const batch of batches) {
if (batch === this) {
is_earlier = false;
continue;
}
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier) {
// bring the value up to date
batch.current.set(source, value);
} else {
// later batch has more recent value,
// no need to re-run these effects
continue;
}
}
mark_effects(source);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
const revert = Batch.apply(batch);
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
}
queued_root_effects = [];
revert();
}
}
current_batch = null;
}
batches.delete(this);
}
increment() {
@ -427,9 +386,6 @@ export class Batch {
schedule_effect(e);
}
this.#render_effects = [];
this.#effects = [];
this.flush();
} else {
this.deactivate();
@ -467,11 +423,52 @@ export class Batch {
/** @param {() => void} task */
static enqueue(task) {
if (tasks.length === 0) {
queueMicrotask(dequeue);
queue_micro_task(task);
}
tasks.unshift(task);
/**
* @param {Batch} current_batch
*/
static apply(current_batch) {
if (!async_mode_flag || batches.size === 1) {
return noop;
}
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
/** @type {Map<Source, { v: unknown, wv: number }>} */
var current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of current_batch.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
for (const batch of batches) {
if (batch === current_batch) continue;
for (const [source, previous] of batch.#previous) {
if (!current_values.has(source)) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous;
}
}
}
return () => {
for (const [source, { v, wv }] of current_values) {
// reset the source to the current value (unless
// it got a newer value as a result of effects running)
if (source.wv <= wv) {
source.v = v;
}
}
batch_deriveds = null;
};
}
}
@ -495,7 +492,10 @@ export function flushSync(fn) {
var result;
if (fn) {
if (current_batch !== null) {
flush_effects();
}
result = fn();
}
@ -641,6 +641,26 @@ function flush_queued_effects(effects) {
eager_block_effects = null;
}
/**
* This is similar to `mark_reactions`, but it only marks async/block effects
* so that these can re-run after another batch has been committed
* @param {Value} value
*/
function mark_effects(value) {
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction));
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
}
}
}
/**
* @param {Effect} signal
* @returns {void}
@ -667,28 +687,6 @@ export function schedule_effect(signal) {
queued_root_effects.push(effect);
}
export function suspend() {
var boundary = get_pending_boundary();
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
boundary.update_pending_count(1);
if (!pending) batch.increment();
return function unsuspend() {
boundary.update_pending_count(-1);
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context();
};
}
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/

@ -26,15 +26,16 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect } from './effects.js';
import { async_effect, destroy_effect, teardown } from './effects.js';
import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_deriveds, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.js';
/** @type {Effect | null} */
export let current_async_effect = null;
@ -109,37 +110,40 @@ export function async_derived(fn, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
/** @type {Promise<V> | null} */
var prev = null;
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
async_effect(() => {
if (DEV) current_async_effect = active_effect;
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
try {
var p = fn();
// Make sure to always access the then property to read any signals
// it might access, so that we track them as dependencies.
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
Promise.resolve(fn()).then(d.resolve, d.reject);
} catch (error) {
p = Promise.reject(error);
d.reject(error);
}
if (DEV) current_async_effect = null;
var r = () => p;
promise = prev?.then(r, r) ?? Promise.resolve(p);
prev = promise;
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.pending;
var pending = boundary.is_pending();
if (should_suspend) {
boundary.update_pending_count(1);
if (!pending) batch.increment();
if (!pending) {
batch.increment();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.set(batch, d);
}
}
/**
@ -147,8 +151,6 @@ export function async_derived(fn, location) {
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
prev = null;
current_async_effect = null;
if (!pending) batch.activate();
@ -187,12 +189,12 @@ export function async_derived(fn, location) {
unset_context();
};
promise.then(handler, (e) => handler(null, e || 'unknown'));
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
});
if (batch) {
return () => {
queueMicrotask(() => batch.neuter());
};
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
}
});
@ -231,7 +233,7 @@ export function async_derived(fn, location) {
export function user_derived(fn) {
const d = derived(fn);
push_reaction_value(d);
if (!async_mode_flag) push_reaction_value(d);
return d;
}

@ -11,7 +11,7 @@ import {
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js';
import { component_root, branch } from './reactivity/effects.js';
import { component_root } from './reactivity/effects.js';
import {
hydrate_next,
hydrate_node,
@ -30,7 +30,8 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE } from './constants.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
* This is normally true block effects should run their intro transitions
@ -119,19 +120,9 @@ export function hydrate(component, options) {
set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor));
hydrate_next();
const instance = _mount(component, { ...options, anchor });
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
set_hydrating(false);
return /** @type {Exports} */ (instance);
@ -152,7 +143,7 @@ export function hydrate(component, options) {
e.hydration_failed();
}
// If an error occured above, the operations might not yet have been initialised.
// If an error occurred above, the operations might not yet have been initialised.
init_operations();
clear_text_content(target);
@ -218,7 +209,12 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text());
branch(() => {
boundary(
/** @type {TemplateNode} */ (anchor_node),
{
pending: () => {}
},
(anchor_node) => {
if (context) {
push({});
var ctx = /** @type {ComponentContext} */ (component_context);
@ -241,12 +237,22 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
if (hydrating) {
/** @type {Effect} */ (active_effect).nodes_end = hydrate_node;
if (
hydrate_node === null ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
}
if (context) {
pop();
}
});
}
);
return () => {
for (var event_name of registered_events) {
@ -309,8 +315,12 @@ export function unmount(component, options) {
}
if (DEV) {
if (STATE_SYMBOL in component) {
w.state_proxy_unmount();
} else {
w.lifecycle_double_unmount();
}
}
return Promise.resolve();
}

@ -500,7 +500,13 @@ export function update_effect(effect) {
*/
export async function tick() {
if (async_mode_flag) {
return new Promise((f) => requestAnimationFrame(() => f()));
return new Promise((f) => {
// Race them against each other - in almost all cases requestAnimationFrame will fire first,
// but e.g. in case the window is not focused or a view transition happens, requestAnimationFrame
// will be delayed and setTimeout helps us resolve fast enough in that case
requestAnimationFrame(() => f());
setTimeout(() => f());
});
}
await Promise.resolve();

@ -224,6 +224,17 @@ export function state_proxy_equality_mismatch(operator) {
}
}
/**
* Tried to unmount a state proxy, rather than a component
*/
export function state_proxy_unmount() {
if (DEV) {
console.warn(`%c[svelte] state_proxy_unmount\n%cTried to unmount a state proxy, rather than a component\nhttps://svelte.dev/e/state_proxy_unmount`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/state_proxy_unmount`);
}
}
/**
* A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/

@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */
/** @import { Payload } from '../payload' */
/** @import { Renderer } from '../renderer' */
/** @import { Getters } from '#shared' */
/**
@ -13,9 +13,9 @@
*/
export function createRawSnippet(fn) {
// @ts-expect-error the types are a lie
return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
return (/** @type {Renderer} */ renderer, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out.push(
renderer.push(
fn(...getters)
.render()
.trim()

@ -1,10 +1,14 @@
/** @import { Component } from '#server' */
/** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env';
import { on_destroy } from './index.js';
import * as e from './errors.js';
/** @type {Component | null} */
export var current_component = null;
/** @type {SSRContext | null} */
export var ssr_context = null;
/** @param {SSRContext | null} v */
export function set_ssr_context(v) {
ssr_context = v;
}
/**
* @template T
@ -47,42 +51,35 @@ export function getAllContexts() {
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (current_component === null) {
if (ssr_context === null) {
e.lifecycle_outside_component(name);
}
return (current_component.c ??= new Map(get_parent_context(current_component) || undefined));
return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined));
}
/**
* @param {Function} [fn]
*/
export function push(fn) {
current_component = { p: current_component, c: null, d: null };
ssr_context = { p: ssr_context, c: null, r: null };
if (DEV) {
// component function
current_component.function = fn;
ssr_context.function = fn;
ssr_context.element = ssr_context.p?.element;
}
}
export function pop() {
var component = /** @type {Component} */ (current_component);
var ondestroy = component.d;
if (ondestroy) {
on_destroy.push(...ondestroy);
}
current_component = component.p;
ssr_context = /** @type {SSRContext} */ (ssr_context).p;
}
/**
* @param {Component} component_context
* @param {SSRContext} ssr_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
function get_parent_context(ssr_context) {
let parent = ssr_context.p;
while (parent !== null) {
const context_map = parent.c;
@ -94,3 +91,22 @@ function get_parent_context(component_context) {
return null;
}
/**
* Wraps an `await` expression in such a way that the component context that was
* active before the expression evaluated can be reapplied afterwards
* `await a + b()` becomes `(await $.save(a))() + b()`, meaning `b()` will have access
* to the context of its component.
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save(promise) {
var previous_context = ssr_context;
var value = await promise;
return () => {
ssr_context = previous_context;
return value;
};
}

@ -1,36 +1,35 @@
/** @import { Component } from '#server' */
/** @import { SSRContext } from '#server' */
import { FILENAME } from '../../constants.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
import { set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js';
import { Renderer } from './renderer.js';
// TODO move this
/**
* @typedef {{
* tag: string;
* parent: null | Element;
* filename: null | string;
* parent: undefined | Element;
* filename: undefined | string;
* line: number;
* column: number;
* }} Element
*/
/**
* @type {Element | null}
* This is exported so that it can be cleared between tests
* @type {Set<string>}
*/
let parent = null;
/** @type {Set<string>} */
let seen;
export let seen;
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {string} message
*/
function print_error(payload, message) {
function print_error(renderer, message) {
message =
`node_invalid_placement_ssr: ${message}\n\n` +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
@ -40,28 +39,22 @@ function print_error(payload, message) {
// eslint-disable-next-line no-console
console.error(message);
payload.head.out.push(`<script>console.error(${JSON.stringify(message)})</script>`);
}
export function reset_elements() {
let old_parent = parent;
parent = null;
return () => {
parent = old_parent;
};
renderer.head((r) => r.push(`<script>console.error(${JSON.stringify(message)})</script>`));
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
* @param {string} tag
* @param {number} line
* @param {number} column
*/
export function push_element(payload, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME];
var child = { tag, parent, filename, line, column };
export function push_element(renderer, tag, line, column) {
var context = /** @type {SSRContext} */ (ssr_context);
var filename = context.function[FILENAME];
var parent = context.element;
var element = { tag, parent, filename, line, column };
if (parent !== null) {
if (parent !== undefined) {
var ancestor = parent.parent;
var ancestors = [parent.tag];
@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
if (message) print_error(payload, message);
if (message) print_error(renderer, message);
while (ancestor != null) {
ancestors.push(ancestor.tag);
@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
if (message) print_error(payload, message);
if (message) print_error(renderer, message);
ancestor = ancestor.parent;
}
}
parent = child;
set_ssr_context({ ...context, p: context, element });
}
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
set_ssr_context(/** @type {SSRContext} */ (ssr_context).p);
}
/**
* @param {Payload} payload
* @param {Renderer} renderer
*/
export function validate_snippet_args(payload) {
export function validate_snippet_args(renderer) {
if (
typeof payload !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
typeof renderer !== 'object' ||
// for some reason typescript consider the type of renderer as never after the first instanceof
!(renderer instanceof Renderer)
) {
e.invalid_snippet_arguments();
}

@ -2,6 +2,30 @@
export * from '../shared/errors.js';
/**
* Encountered asynchronous work while rendering synchronously.
* @returns {never}
*/
export function await_invalid() {
const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`);
error.name = 'Svelte error';
throw error;
}
/**
* The `html` property of server render results has been deprecated. Use `body` instead.
* @returns {never}
*/
export function html_deprecated() {
const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`);
error.name = 'Svelte error';
throw error;
}
/**
* `%name%(...)` is not available on the server
* @param {string} name

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

Loading…
Cancel
Save