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: issue_comment:
types: [created] types: [created]
permissions: {}
jobs: jobs:
trigger: trigger:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -15,7 +17,6 @@ jobs:
contents: read # to clone the repo contents: read # to clone the repo
steps: steps:
- name: monitor action permissions - name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission - name: check user authorization # user needs triage permission
uses: actions/github-script@v7 uses: actions/github-script@v7
id: check-permissions id: check-permissions

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

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

@ -17,7 +17,6 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -27,7 +26,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 24.x
cache: pnpm cache: pnpm
- name: Install - name: Install
@ -45,4 +44,3 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true 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/errors.js
packages/svelte/src/internal/shared/warnings.js packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.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/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected* packages/svelte/tests/**/_expected*

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved ## 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). - 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). - 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 #### 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 #### 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 ## 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). >[!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 ## 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>` ## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`: Checkbox inputs can be bound with `bind:checked`:
```svelte ```svelte
<label> <label>
@ -117,6 +117,8 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form> </form>
``` ```
> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
## `<input bind:indeterminate>` ## `<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: 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. 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 ## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)... When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/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 ## 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: 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). 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 ## 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. 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 ## 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). 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/). 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. 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 ### 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): 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). 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/). 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? ## 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? ## How do I test Svelte apps?
@ -91,17 +91,9 @@ Some resources for getting started with testing:
## Is there a router? ## 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. However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).
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).
## How do I write a mobile app with Svelte? ## 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. 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 ### 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 ### 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 ### a11y_distracting_elements

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

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

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

@ -1,5 +1,127 @@
# svelte # 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 ## 5.38.6
### Patch Changes ### 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. 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 ## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called > 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 ## 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 ## 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 ## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead > Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.38.6", "version": "5.39.8",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -158,7 +158,7 @@
"@types/aria-query": "^5.0.4", "@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"dts-buddy": "^0.5.5", "dts-buddy": "^0.5.5",
"esbuild": "^0.21.5", "esbuild": "^0.25.10",
"rollup": "^4.22.4", "rollup": "^4.22.4",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"tinyglobby": "^0.2.12", "tinyglobby": "^0.2.12",

@ -401,6 +401,7 @@ function run() {
transform('client-warnings', 'src/internal/client/warnings.js'); transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.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('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js'); transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.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,13 +85,15 @@ declare namespace $state {
? NonReactive<T> ? NonReactive<T>
: T extends { toJSON(): infer R } : T extends { toJSON(): infer R }
? R ? R
: T extends Array<infer U> : T extends readonly unknown[]
? Array<Snapshot<U>> ? { [K in keyof T]: Snapshot<T[K]> }
: T extends object : T extends Array<infer U>
? T extends { [key: string]: any } ? Array<Snapshot<U>>
? { [K in keyof T]: Snapshot<T[K]> } : T extends object
: never ? T extends { [key: string]: any }
: never; ? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
/** /**
* Declares state that is _not_ made deeply reactive instead of mutating it, * Declares state that is _not_ made deeply reactive instead of mutating it,

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

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

@ -27,6 +27,11 @@ export interface AnalysisState {
// legacy stuff // legacy stuff
reactive_statement: null | ReactiveStatement; 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< 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 { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
/** /**
@ -7,16 +8,25 @@ import * as e from '../../../errors.js';
* @param {Context} context * @param {Context} context
*/ */
export function AwaitExpression(node, 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) { if (context.state.expression) {
context.state.expression.has_await = true; context.state.expression.has_await = true;
if ( if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
context.state.fragment.metadata.has_await = true; context.state.fragment.metadata.has_await = true;
} }
@ -37,3 +47,101 @@ export function AwaitExpression(node, context) {
context.next(); 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( e.bind_invalid_target(
node, node,
node.name, 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 { } else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') { 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') { 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( e.bind_invalid_target(
node, node,
node.name, 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.next({
...context.state, ...context.state,
function_depth: context.state.function_depth + 1, function_depth: context.state.function_depth + 1,
in_derived: true,
expression expression
}); });

@ -35,5 +35,9 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
context.visit(declaration.id); 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]; 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); 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 (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node); e.props_invalid_identifier(node);

@ -382,7 +382,10 @@ export function check_element(node, context) {
} }
// element-specific checks // 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) { switch (node.name) {
case 'a': case 'a':

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

@ -22,11 +22,6 @@ export interface ClientTransformState extends TransformState {
*/ */
readonly in_constructor: boolean; readonly in_constructor: boolean;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
readonly in_derived: boolean;
/** `true` if we're transforming the contents of `<script>` */ /** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean; readonly is_instance: boolean;

@ -5,7 +5,7 @@
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
/** @import { Visitor } from 'zimmerframe' */ /** @import { Visitor } from 'zimmerframe' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js'; import { is_simple_expression, save } from '../../../utils/ast.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -360,7 +360,7 @@ export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async); const thunk = b.thunk(expression, async);
if (async) { if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk)))); return save(b.call('$.async_derived', thunk));
} else { } else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); 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 { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, is_ignored } from '../../../../state.js';
import { save } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
/** /**
@ -10,13 +11,8 @@ import * as b from '../../../../utils/builders.js';
export function AwaitExpression(node, context) { export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument)); const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1; if (context.state.analysis.pickled_awaits.has(node)) {
return save(argument);
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
return b.call(b.await(b.call('$.save', argument)));
} }
// in dev, note which values are read inside a reactive expression, // 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 }; 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':
case '$derived.by': { case '$derived.by': {
let fn = /** @type {Expression} */ ( let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); 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]; const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
const init = build_expression( const init = build_expression(context, declaration.init, node.metadata.expression);
{ ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
let expression = create_derived(context.state, init, node.metadata.expression.has_await); 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']} */ ({ const child_state = /** @type {ComponentContext['state']} */ ({
...context.state, ...context.state,
transform, transform
in_derived: true
}); });
// TODO optimise the simple `{ x } = y` case — we can just return `y` // 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_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; 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 { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js'; import { build_getter } from '../utils.js';
import { import {
@ -72,6 +72,7 @@ export function RegularElement(node, context) {
let has_spread = node.metadata.has_spread; let has_spread = node.metadata.has_spread;
let has_use = false; let has_use = false;
let should_remove_defaults = false;
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
switch (attribute.type) { switch (attribute.type) {
@ -172,7 +173,12 @@ export function RegularElement(node, context) {
bindings.has('group') || bindings.has('group') ||
(!bindings.has('group') && has_value_attribute)) (!bindings.has('group') && has_value_attribute))
) { ) {
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); 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)));
}
} }
} }
@ -202,7 +208,15 @@ export function RegularElement(node, context) {
bindings.has('checked'); bindings.has('checked');
if (has_spread) { 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 { } else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) { if (is_event_attribute(attribute)) {
@ -392,10 +406,24 @@ export function RegularElement(node, context) {
} }
if (!has_spread && needs_special_value_handling) { if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (node.metadata.synthetic_value_node) {
if (attribute.name === 'value') { const synthetic_node = node.metadata.synthetic_value_node;
build_element_special_value_attribute(node.name, node_id, attribute, context); const synthetic_attribute = create_attribute(
break; '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);
break;
}
} }
} }
} }
@ -631,8 +659,15 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id * @param {Identifier} node_id
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {ComponentContext} context * @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 state = context.state;
const is_select_with_value = const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change, // 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 evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), 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'), b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal('')) 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( const update = b.stmt(
is_select_with_value is_select_with_value
? b.sequence([ ? b.sequence([
inner_assignment, set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}> // 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 // 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 // because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice. // mutation observer wouldn't notice.
b.call('$.select_option', node_id, value) b.call('$.select_option', node_id, value)
]) ])
: inner_assignment : synthetic
? assignment
: set_value_assignment
); );
if (has_state) { if (has_state) {

@ -12,8 +12,19 @@ export function TitleElement(node, context) {
/** @type {any} */ (node.fragment.nodes), /** @type {any} */ (node.fragment.nodes),
context 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) { if (has_state) {
context.state.update.push(statement); context.state.update.push(statement);

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

@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') { } else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute)); const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
props_and_spreads.push( props_and_spreads.push(
b.thunk( b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call 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 {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element * @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id * @param {Identifier} element_id
* @param {boolean} [should_remove_defaults]
*/ */
export function build_attribute_effect( export function build_attribute_effect(
attributes, attributes,
@ -23,7 +24,8 @@ export function build_attribute_effect(
style_directives, style_directives,
context, context,
element, element,
element_id element_id,
should_remove_defaults = false
) { ) {
/** @type {ObjectExpression['properties']} */ /** @type {ObjectExpression['properties']} */
const values = []; const values = [];
@ -91,6 +93,7 @@ export function build_attribute_effect(
element.metadata.scoped && element.metadata.scoped &&
context.state.analysis.css.hash !== '' && context.state.analysis.css.hash !== '' &&
b.literal(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 is_ignored(element, 'hydration_attribute_changed') && b.true
) )
) )

@ -6,7 +6,7 @@ import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js'; import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js'; import { extract_identifiers } from '../../../utils/ast.js';
import * as b from '#compiler/builders'; 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 { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -40,6 +40,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_component_renderer, create_async_block } from './visitors/shared/utils.js';
/** @type {Visitors} */ /** @type {Visitors} */
const global_visitors = { const global_visitors = {
@ -187,23 +188,21 @@ export function server_component(analysis, options) {
template.body = [ template.body = [
...snippets, ...snippets,
b.let('$$settled', b.true), b.let('$$settled', b.true),
b.let('$$inner_payload'), b.let('$$inner_renderer'),
b.function_declaration( b.function_declaration(
b.id('$$render_inner'), b.id('$$render_inner'),
[b.id('$$payload')], [b.id('$$renderer')],
b.block(/** @type {Statement[]} */ (rest)) b.block(/** @type {Statement[]} */ (rest))
), ),
b.do_while( b.do_while(
b.unary('!', b.id('$$settled')), b.unary('!', b.id('$$settled')),
b.block([ b.block([
b.stmt(b.assignment('=', b.id('$$settled'), b.true)), b.stmt(b.assignment('=', b.id('$$settled'), b.true)),
b.stmt( b.stmt(b.assignment('=', b.id('$$inner_renderer'), b.call('$$renderer.copy'))),
b.assignment('=', b.id('$$inner_payload'), b.call('$.copy_payload', b.id('$$payload'))) b.stmt(b.call('$$render_inner', b.id('$$inner_renderer')))
),
b.stmt(b.call('$$render_inner', b.id('$$inner_payload')))
]) ])
), ),
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)))); 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[]} */ (instance.body),
.../** @type {Statement[]} */ (template.body) .../** @type {Statement[]} */ (template.body)
]); ]);
if (analysis.instance.has_await) {
component_block = b.block([create_async_block(component_block)]);
}
// trick esrap into including comments // trick esrap into including comments
component_block.loc = instance.loc; component_block.loc = instance.loc;
if (analysis.props_id) { if (analysis.props_id) {
// need to be placed on first line of the component for hydration // need to be placed on first line of the component for hydration
component_block.body.unshift( 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; let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) { if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', dev && b.id(analysis.name)))); component_block = b.block([
component_block.body.push(b.stmt(b.call('$.pop'))); call_component_renderer(component_block, dev && b.id(component_name))
]);
} }
if (analysis.uses_rest_props) { 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); 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)]))); 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 = let should_inject_props =
@ -311,7 +315,7 @@ export function server_component(analysis, options) {
const component_function = b.function_declaration( const component_function = b.function_declaration(
b.id(analysis.name), 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 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 { return {
type: 'Program', type: 'Program',
sourceType: 'module', sourceType: 'module',

@ -1,29 +1,33 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */ /** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; 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 {AST.AwaitBlock} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function AwaitBlock(node, context) { export function AwaitBlock(node, context) {
context.state.template.push( /** @type {Statement} */
b.stmt( let statement = b.stmt(
b.call( b.call(
'$.await', '$.await',
b.id('$$payload'), b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)), /** @type {Expression} */ (context.visit(node.expression)),
b.thunk( b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
), ),
b.arrow( b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [], node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([]) 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 { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types' */
import * as b from '../../../../utils/builders.js'; import { save } from '../../../../utils/ast.js';
/** /**
* @param {AwaitExpression} node * @param {AwaitExpression} node
* @param {Context} context * @param {Context} context
*/ */
export function AwaitExpression(node, context) { export function AwaitExpression(node, context) {
// if `await` is inside a function, or inside `<script module>`, const argument = /** @type {Expression} */ (context.visit(node.argument));
// allow it, otherwise error
if ( if (context.state.analysis.pickled_awaits.has(node)) {
context.state.scope.function_depth === 0 || return save(argument);
context.path.some( }
(node) =>
node.type === 'ArrowFunctionExpression' || // we also need to restore context after block expressions
node.type === 'FunctionDeclaration' || let i = context.path.length;
node.type === 'FunctionExpression' while (i--) {
) const parent = context.path[i];
) {
return context.next(); if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
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 { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 * @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); 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'); 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[]} */ /** @type {Statement[]} */
const each = []; const each = [];
@ -44,23 +45,27 @@ export function EachBlock(node, context) {
); );
if (node.fallback) { 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)); const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift( fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push( block.body.push(
b.if( b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]), b.block([open, for_loop]),
fallback fallback
), )
block_close
); );
} else { } 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) { export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression)); 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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 * @param {AST.IfBlock} node
@ -17,13 +16,16 @@ export function IfBlock(node, context) {
? /** @type {BlockStatement} */ (context.visit(node.alternate)) ? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]); : b.block([]);
consequent.body.unshift( consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open))
);
alternate.body.unshift( alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE)))
);
context.state.template.push(b.if(test, consequent, alternate), block_close); /** @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 { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.js'; import { build_element_attributes, prepare_element_spread_object } from './shared/element.js';
import { process_children, build_template, build_attribute_value } from './shared/utils.js'; import {
process_children,
build_template,
create_child_block,
PromiseOptimiser
} from './shared/utils.js';
/** /**
* @param {AST.RegularElement} node * @param {AST.RegularElement} node
@ -22,21 +27,56 @@ export function RegularElement(node, context) {
...context.state, ...context.state,
namespace, namespace,
preserve_whitespace: 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); const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`)); const optimiser = new PromiseOptimiser();
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance // 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) { 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(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`) 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; return;
} }
@ -63,7 +103,7 @@ export function RegularElement(node, context) {
b.stmt( b.stmt(
b.call( b.call(
'$.push_element', '$.push_element',
b.id('$$payload'), b.id('$$renderer'),
b.literal(node.name), b.literal(node.name),
b.literal(location.line), b.literal(location.line),
b.literal(location.column) 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 fn = b.arrow(
const value = node.attributes.find( [b.id('$$renderer')],
(attribute) => b.block([...state.init, ...build_template(inner_state.template)])
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value'
); );
if (node.attributes.some((attribute) => attribute.type === 'SpreadAttribute')) {
select_with_value = true; const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
state.template.push(
b.stmt( const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest));
b.assignment(
'=', if (optimiser.expressions.length > 0) {
b.id('$$payload.select_value'), context.state.template.push(
b.member( create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
)
); );
} else if (value) { } else {
select_with_value = true; context.state.template.push(...state.init, statement);
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))
)
)
);
}
} }
return;
} }
if ( if (is_option_special) {
node.name === 'option' && let body;
!node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
)
) {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push( if (node.metadata.synthetic_value_node) {
b.stmt( body = optimiser.transform(
b.call( node.metadata.synthetic_value_node.expression,
'$.valueless_option', node.metadata.synthetic_value_node.metadata.expression
b.id('$$payload'), );
b.thunk(b.block([...inner_state.init, ...build_template(inner_state.template)])) } else {
) const inner_state = { ...state, template: [], init: [] };
) process_children(trimmed, { ...context, state: inner_state });
);
} else if (body !== null) { body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
}
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 // if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy // the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] }; const inner_state = { ...state, template: [], init: [] };
@ -174,10 +194,6 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state }); 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) { if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`)); state.template.push(b.literal(`</${node.name}>`));
} }
@ -185,4 +201,16 @@ export function RegularElement(node, context) {
if (dev) { if (dev) {
state.template.push(b.stmt(b.call('$.pop_element'))); 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( b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
b.id('$$payload'), b.id('$$renderer'),
...snippet_args ...snippet_args
) )
) )

@ -2,7 +2,13 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; 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 * @param {AST.SlotElement} node
@ -15,13 +21,22 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */ /** @type {Expression[]} */
const spreads = []; const spreads = [];
const optimiser = new PromiseOptimiser();
let name = b.literal('default'); let name = b.literal('default');
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') { 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') { } 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') { if (attribute.name === 'name') {
name = /** @type {Literal} */ (value); name = /** @type {Literal} */ (value);
@ -43,12 +58,17 @@ export function SlotElement(node, context) {
const slot = b.call( const slot = b.call(
'$.slot', '$.slot',
b.id('$$payload'), b.id('$$renderer'),
b.id('$$props'), b.id('$$props'),
name, name,
props_expression, props_expression,
fallback 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 { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';
/** /**
* @param {AST.SnippetBlock} node * @param {AST.SnippetBlock} node
@ -11,17 +12,21 @@ import * as b from '#compiler/builders';
export function SnippetBlock(node, context) { export function SnippetBlock(node, context) {
let fn = b.function_declaration( let fn = b.function_declaration(
node.expression, node.expression,
[b.id('$$payload'), ...node.parameters], [b.id('$$renderer'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body)) /** @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 // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true; fn.___snippet = true;
const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init; const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init;
if (dev) { 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))); statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id)));
} }

@ -1,21 +1,30 @@
/** @import { BlockStatement } from 'estree' */ /** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; 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 {AST.SvelteBoundary} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function SvelteBoundary(node, context) { export function SvelteBoundary(node, context) {
context.state.template.push(b.literal(BLOCK_OPEN));
// if this has a `pending` snippet, render it // if this has a `pending` snippet, render it
const pending_attribute = /** @type {AST.Attribute} */ ( const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending') 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} */ ( const pending_snippet = /** @type {AST.SnippetBlock} */ (
node.fragment.nodes.find( node.fragment.nodes.find(
@ -23,16 +32,44 @@ export function SvelteBoundary(node, context) {
) )
); );
if (pending_attribute) { if (pending_attribute || pending_snippet) {
const value = build_attribute_value(pending_attribute.value, context, false, true); if (pending_attribute && is_pending_attr_nullish && !pending_snippet) {
context.state.template.push(b.call(value, b.id('$$payload'))); const callee = build_attribute_value(
} else if (pending_snippet) { pending_attribute.value,
context.state.template.push( context,
/** @type {BlockStatement} */ (context.visit(pending_snippet.body)) (expression) => expression,
); false,
true
);
const pending = b.call(callee, b.id('$$renderer'));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.if(
callee,
b.block(build_template([block_open_else, pending, block_close])),
b.block(build_template([block_open, block, block_close]))
)
);
} else {
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 { } else {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment))); 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 { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */ /** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import { dev, locator } from '../../../../state.js'; import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js'; import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.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 * @param {AST.SvelteElement} node
@ -37,7 +37,9 @@ export function SvelteElement(node, context) {
init: [] init: []
}; };
build_element_attributes(node, { ...context, state }); const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) { if (dev) {
const location = /** @type {Location} */ (locator(node.start)); const location = /** @type {Location} */ (locator(node.start));
@ -45,7 +47,7 @@ export function SvelteElement(node, context) {
b.stmt( b.stmt(
b.call( b.call(
'$.push_element', '$.push_element',
b.id('$$payload'), b.id('$$renderer'),
tag, tag,
b.literal(location.line), b.literal(location.line),
b.literal(location.column) b.literal(location.column)
@ -57,18 +59,23 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]); const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state)); const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push( /** @type {Statement} */
b.stmt( let statement = b.stmt(
b.call( b.call(
'$.element', '$.element',
b.id('$$payload'), b.id('$$renderer'),
tag, tag,
attributes.body.length > 0 && b.thunk(attributes), attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children) 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) { if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element'))); 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)); const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push( 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 } }); process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>')); 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 { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @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 * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js'; import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.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) { for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') { if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) { if (!slot_scope_applies_to_itself) {
lets.default.push(attribute); lets.default.push(attribute);
} }
} else if (attribute.type === 'SpreadAttribute') { } 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') { } else if (attribute.type === 'Attribute') {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true
);
if (attribute.name.startsWith('--')) { if (attribute.name.startsWith('--')) {
const value = build_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value)); custom_css_props.push(b.init(attribute.name, value));
continue; continue;
} }
@ -90,7 +106,6 @@ export function build_inline_component(node, expression, context) {
has_children_prop = true; has_children_prop = true;
} }
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SequenceExpression') { if (attribute.expression.type === 'SequenceExpression') {
@ -201,7 +216,7 @@ export function build_inline_component(node, expression, context) {
if (block.body.length === 0) continue; if (block.body.length === 0) continue;
/** @type {Pattern[]} */ /** @type {Pattern[]} */
const params = [b.id('$$payload')]; const params = [b.id('$$renderer')];
if (lets[slot_name].length > 0) { if (lets[slot_name].length > 0) {
const pattern = b.object_pattern( const pattern = b.object_pattern(
@ -227,7 +242,12 @@ export function build_inline_component(node, expression, context) {
params.push(pattern); 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 (slot_name === 'default' && !has_children_prop) {
if ( if (
@ -278,7 +298,7 @@ export function build_inline_component(node, expression, context) {
let statement = b.stmt( let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)( (node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
expression, expression,
b.id('$$payload'), b.id('$$renderer'),
props_expression props_expression
) )
); );
@ -291,27 +311,33 @@ export function build_inline_component(node, expression, context) {
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) { if (custom_css_props.length > 0) {
context.state.template.push( statement = b.stmt(
b.stmt( b.call(
b.call( '$.css_props',
'$.css_props', b.id('$$renderer'),
b.id('$$payload'), b.literal(context.state.namespace === 'svg' ? false : true),
b.literal(context.state.namespace === 'svg' ? false : true), b.object(custom_css_props),
b.object(custom_css_props), b.thunk(b.block([statement])),
b.thunk(b.block([statement])), dynamic && b.true
dynamic && b.true
)
) )
); );
} else { }
if (dynamic) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement); if (optimiser.expressions.length > 0) {
statement = create_async_block(b.block([optimiser.apply(), statement]));
}
if (!context.state.skip_hydration_boundaries) { if (dynamic && custom_css_props.length === 0) {
context.state.template.push(empty_comment); context.state.template.push(empty_comment);
} }
context.state.template.push(statement);
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 { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js'; import { binding_properties } from '../../../../bindings.js';
@ -11,6 +11,7 @@ import {
import { regex_starts_with_newline } from '../../../../patterns.js'; import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { import {
ELEMENT_IS_INPUT,
ELEMENT_IS_NAMESPACED, ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE ELEMENT_PRESERVE_ATTRIBUTE_CASE
} from '../../../../../../constants.js'; } 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. * their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context * @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>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -61,7 +63,8 @@ export function build_element_attributes(node, context) {
// also see related code in analysis phase // also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data; 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') { } else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no // 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) // 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 expression: is_checkbox
? b.call( ? b.call(
b.member(attribute.expression, 'includes'), b.member(attribute.expression, 'includes'),
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, transform)
) )
: b.binary( : b.binary(
'===', '===',
attribute.expression, attribute.expression,
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, transform)
), ),
metadata: { metadata: {
expression: create_expression_metadata() expression: create_expression_metadata()
@ -201,30 +204,14 @@ export function build_element_attributes(node, context) {
} }
if (has_spread) { if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context); build_element_spread_attributes(
if (node.name === 'option') { node,
context.state.template.push( attributes,
b.call( style_directives,
'$.maybe_selected', class_directives,
b.id('$$payload'), context,
b.member( transform
build_spread_object( );
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
);
}
} else { } else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; 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( build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
) )
).value; ).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; continue;
} }
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) 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)}"`)); context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') { } 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') { } 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 { } else {
context.state.template.push( context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) 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 {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
export function build_spread_object(element, attributes, context) { export function build_spread_object(element, attributes, context, transform) {
return b.object( const object = b.object(
attributes.map((attribute) => { attributes.map((attribute) => {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
); );
return b.prop('init', b.key(name), value); return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
@ -345,12 +325,20 @@ export function build_spread_object(element, attributes, context) {
attribute.expression.type === 'SequenceExpression' attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0]) ? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression)); : /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value); 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.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
function build_element_spread_attributes( function build_element_spread_attributes(
element, element,
attributes, attributes,
style_directives, style_directives,
class_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; let classes;
/** @type {ObjectExpression | undefined} */
let styles; let styles;
let flags = 0; let flags = 0;
@ -378,9 +441,13 @@ function build_element_spread_attributes(
directive.name, directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(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); classes = b.object(properties);
} }
@ -390,10 +457,9 @@ function build_element_spread_attributes(
directive.name, directive.name,
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true) : build_attribute_value(directive.value, context, transform, true)
) )
); );
styles = b.object(properties); styles = b.object(properties);
} }
@ -401,17 +467,17 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
} else if (is_custom_element_node(element)) { } else if (is_custom_element_node(element)) {
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; 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 = const css_hash =
element.metadata.scoped && context.state.analysis.css.hash element.metadata.scoped && context.state.analysis.css.hash
? b.literal(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]; return [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
} }
/** /**
@ -420,8 +486,9 @@ function build_element_spread_attributes(
* @param {Expression} expression * @param {Expression} expression
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {string | null} hash * @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} */ /** @type {ObjectExpression | undefined} */
let directives; let directives;
@ -431,7 +498,10 @@ function build_attr_class(class_directives, expression, context, hash) {
b.prop( b.prop(
'init', 'init',
b.literal(directive.name), 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 {AST.StyleDirective[]} style_directives
* @param {Expression} expression * @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} */ /** @type {ArrayExpression | ObjectExpression | undefined} */
let directives; let directives;
@ -468,7 +539,7 @@ function build_attr_style(style_directives, expression, context) {
const expression = const expression =
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true); : build_attribute_value(directive.value, context, transform, true);
let name = directive.name; let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') { if (name[0] !== '-' || name[1] !== '-') {

@ -1,20 +1,25 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */ /** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
import { import {
BLOCK_CLOSE, BLOCK_CLOSE,
BLOCK_OPEN, BLOCK_OPEN,
BLOCK_OPEN_ELSE,
EMPTY_COMMENT EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js'; } from '../../../../../../internal/server/hydration.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.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 */ /** 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); 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 */ /** 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); export const block_close = b.literal(BLOCK_CLOSE);
@ -32,6 +37,10 @@ export function process_children(nodes, { visit, state }) {
let sequence = []; let sequence = [];
function flush() { function flush() {
if (sequence.length === 0) {
return;
}
let quasi = b.quasi('', false); let quasi = b.quasi('', false);
const quasis = [quasi]; const quasis = [quasi];
@ -63,26 +72,25 @@ export function process_children(nodes, { visit, state }) {
} }
state.template.push(b.template(quasis, expressions)); state.template.push(b.template(quasis, expressions));
sequence = [];
} }
for (let i = 0; i < nodes.length; i += 1) { for (const node of nodes) {
const node = nodes[i]; if (node.type === 'ExpressionTag' && node.metadata.expression.has_await) {
flush();
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { 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); sequence.push(node);
} else { } else {
if (sequence.length > 0) { flush();
flush();
sequence = [];
}
visit(node, { ...state }); visit(node, { ...state });
} }
} }
if (sequence.length > 0) { flush();
flush();
}
} }
/** /**
@ -95,11 +103,9 @@ function is_statement(node) {
/** /**
* @param {Array<Statement | Expression>} template * @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator | 'push'} operator
* @returns {Statement[]} * @returns {Statement[]}
*/ */
export function build_template(template, out = b.id('$$payload.out'), operator = 'push') { export function build_template(template) {
/** @type {string[]} */ /** @type {string[]} */
let strings = []; let strings = [];
@ -110,32 +116,18 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = []; const statements = [];
const flush = () => { const flush = () => {
if (operator === 'push') { statements.push(
statements.push( b.stmt(
b.stmt( b.call(
b.call( b.id('$$renderer.push'),
b.member(out, b.id('push')), b.template(
b.template( strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)), expressions
expressions
)
)
)
);
} else {
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
) )
) )
); )
} );
strings = []; strings = [];
expressions = []; expressions = [];
}; };
@ -178,6 +170,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
* *
* @param {AST.Attribute['value']} value * @param {AST.Attribute['value']} value
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @param {boolean} trim_whitespace * @param {boolean} trim_whitespace
* @param {boolean} is_component * @param {boolean} is_component
* @returns {Expression} * @returns {Expression}
@ -185,6 +178,7 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
export function build_attribute_value( export function build_attribute_value(
value, value,
context, context,
transform,
trim_whitespace = false, trim_whitespace = false,
is_component = false is_component = false
) { ) {
@ -203,7 +197,10 @@ export function build_attribute_value(
return b.literal(is_component ? data : escape_html(data, true)); 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); let quasi = b.quasi('', false);
@ -221,7 +218,13 @@ export function build_attribute_value(
: node.data; : node.data;
} else { } else {
expressions.push( 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); quasi = b.quasi('', i + 1 === value.length);
@ -257,3 +260,70 @@ export function build_getter(node, state) {
return node; 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; break;
} }
if (binding.initial?.type === 'SnippetBlock') {
this.is_defined = true;
this.is_known = false;
break;
}
if (!binding.updated && binding.initial !== null && !is_prop) { if (!binding.updated && binding.initial !== null && !is_prop) {
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
break; break;
@ -1032,7 +1038,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}, },
Component: (node, context) => { 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); Component(node, context);
}, },
SvelteSelf: Component, SvelteSelf: Component,

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

@ -105,7 +105,7 @@ export interface CompileOptions extends ModuleCompileOptions {
css?: 'injected' | 'external'; 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. * 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 * @default undefined
*/ */
@ -283,11 +283,16 @@ export type DeclarationKind =
| 'var' | 'var'
| 'let' | 'let'
| 'const' | 'const'
| 'using'
| 'await using'
| 'function' | 'function'
| 'import' | 'import'
| 'param' | 'param'
| 'rest_param' | 'rest_param'
| 'synthetic'; | 'synthetic'
// TODO not yet implemented, but needed for TypeScript reasons
| 'using'
| 'await using';
export interface ExpressionMetadata { export interface ExpressionMetadata {
/** All the bindings that are referenced eagerly (not inside functions) in this expression */ /** All the bindings that are referenced eagerly (not inside functions) in this expression */

@ -344,6 +344,8 @@ export namespace AST {
has_spread: boolean; has_spread: boolean;
scoped: boolean; scoped: boolean;
path: SvelteNode[]; 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.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
: b.binary(/** @type {ESTree.BinaryOperator} */ (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 { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js'; import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js'; import { sanitize_template_string } from './sanitize_template_string.js';
import { has_await } from './ast.js';
/** /**
* @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements * @param {Array<ESTree.Expression | ESTree.SpreadElement | null>} elements
@ -363,7 +364,14 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition} * @returns {ESTree.PropertyDefinition}
*/ */
export function prop_def(key, value, computed = false, is_static = false) { 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) { export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)` // optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') { if (expression.async && expression.body.type === 'AwaitExpression') {
let has_await = false; if (!has_await(expression.body.argument)) {
walk(expression.body.argument, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
}
});
if (!has_await) {
return unthunk(arrow(expression.params, 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) { export function method(kind, key, params, body, computed = false, is_static = false) {
return { return {
type: 'MethodDefinition', type: 'MethodDefinition',
decorators: [],
key, key,
kind, kind,
value: function_builder(null, params, block(body)), value: function_builder(null, params, block(body)),
@ -618,6 +618,7 @@ function if_builder(test, consequent, alternate) {
export function import_all(as, source) { export function import_all(as, source) {
return { return {
type: 'ImportDeclaration', type: 'ImportDeclaration',
attributes: [],
source: literal(source), source: literal(source),
specifiers: [import_namespace(as)] specifiers: [import_namespace(as)]
}; };
@ -631,6 +632,7 @@ export function import_all(as, source) {
export function imports(parts, source) { export function imports(parts, source) {
return { return {
type: 'ImportDeclaration', type: 'ImportDeclaration',
attributes: [],
source: literal(source), source: literal(source),
specifiers: parts.map((p) => ({ specifiers: parts.map((p) => ({
type: 'ImportSpecifier', type: 'ImportSpecifier',

@ -70,8 +70,8 @@ const component_options = {
return input; return input;
}), }),
cssHash: fun(({ css, hash }) => { cssHash: fun(({ css, filename, hash }) => {
return `svelte-${hash(css)}`; return `svelte-${hash(filename === '(unknown)' ? css : filename ?? css)}`;
}), }),
// TODO this is a sourcemap option, would be good to put under a sourcemap namespace // 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 * @param {null | NodeLike} node
*/ */
export function a11y_consider_explicit_label(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_IS_NAMESPACED = 1;
export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1; export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1;
export const ELEMENT_IS_INPUT = 1 << 2;
export const UNINITIALIZED = Symbol(); export const UNINITIALIZED = Symbol();

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

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

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

@ -1,5 +1,11 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */ /** @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 { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js'; import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -15,7 +21,7 @@ import {
hydrate_node, hydrate_node,
hydrating, hydrating,
next, next,
remove_nodes, skip_nodes,
set_hydrate_node set_hydrate_node
} from '../hydration.js'; } from '../hydration.js';
import { get_next_sibling } from '../operations.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 e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { DEV } from 'esm-env'; 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 { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js'; import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -49,16 +55,16 @@ export function boundary(node, props, children) {
} }
export class Boundary { export class Boundary {
pending = false;
/** @type {Boundary | null} */ /** @type {Boundary | null} */
parent; parent;
#pending = false;
/** @type {TemplateNode} */ /** @type {TemplateNode} */
#anchor; #anchor;
/** @type {TemplateNode} */ /** @type {TemplateNode | null} */
#hydrate_open; #hydrate_open = hydrating ? hydrate_node : null;
/** @type {BoundaryProps} */ /** @type {BoundaryProps} */
#props; #props;
@ -81,7 +87,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */ /** @type {DocumentFragment | null} */
#offscreen_fragment = null; #offscreen_fragment = null;
#local_pending_count = 0;
#pending_count = 0; #pending_count = 0;
#is_creating_fallback = false; #is_creating_fallback = false;
/** /**
@ -95,12 +103,12 @@ export class Boundary {
#effect_pending_update = () => { #effect_pending_update = () => {
if (this.#effect_pending) { 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(() => { #effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count); this.#effect_pending = source(this.#local_pending_count);
if (DEV) { if (DEV) {
tag(this.#effect_pending, '$effect.pending()'); tag(this.#effect_pending, '$effect.pending()');
@ -121,44 +129,26 @@ export class Boundary {
this.#props = props; this.#props = props;
this.#children = children; this.#children = children;
this.#hydrate_open = hydrate_node;
this.parent = /** @type {Effect} */ (active_effect).b; this.parent = /** @type {Effect} */ (active_effect).b;
this.pending = !!this.#props.pending; this.#pending = !!this.#props.pending;
this.#effect = block(() => { this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this; /** @type {Effect} */ (active_effect).b = this;
if (hydrating) { if (hydrating) {
const comment = this.#hydrate_open;
hydrate_next(); hydrate_next();
}
const pending = this.#props.pending; const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
if (hydrating && pending) { /** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
this.#pending_effect = branch(() => pending(this.#anchor));
// future work: when we have some form of async SSR, we will
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.pending = false; if (server_rendered_pending) {
} this.#hydrate_pending_content();
}); } else {
this.#hydrate_resolved_content();
}
} else { } else {
try { try {
this.#main_effect = branch(() => children(this.#anchor)); this.#main_effect = branch(() => children(this.#anchor));
@ -169,7 +159,7 @@ export class Boundary {
if (this.#pending_count > 0) { if (this.#pending_count > 0) {
this.#show_pending_snippet(); this.#show_pending_snippet();
} else { } else {
this.pending = false; this.#pending = false;
} }
} }
}, flags); }, flags);
@ -179,6 +169,51 @@ export class Boundary {
} }
} }
#hydrate_resolved_content() {
try {
this.#main_effect = branch(() => this.#children(this.#anchor));
} catch (error) {
this.error(error);
}
// 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));
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.#pending = false;
}
});
}
/**
* 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() { has_pending_snippet() {
return !!this.#props.pending; return !!this.#props.pending;
} }
@ -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) { #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; this.#pending_count += d;
if (this.#pending_count === 0) { if (this.#pending_count === 0) {
this.pending = false; this.#pending = false;
if (this.#pending_effect) { if (this.#pending_effect) {
pause_effect(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) { update_pending_count(d) {
if (this.has_pending_snippet()) { this.#update_pending_count(d);
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); effect_pending_updates.add(this.#effect_pending_update);
} }
@ -261,6 +311,12 @@ export class Boundary {
var onerror = this.#props.onerror; var onerror = this.#props.onerror;
let failed = this.#props.failed; 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) { if (this.#main_effect) {
destroy_effect(this.#main_effect); destroy_effect(this.#main_effect);
this.#main_effect = null; this.#main_effect = null;
@ -277,9 +333,9 @@ export class Boundary {
} }
if (hydrating) { if (hydrating) {
set_hydrate_node(this.#hydrate_open); set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next(); next();
set_hydrate_node(remove_nodes()); set_hydrate_node(skip_nodes());
} }
var did_reset = false; var did_reset = false;
@ -297,7 +353,10 @@ export class Boundary {
e.svelte_boundary_reset_onerror(); 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) { if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => { 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.#main_effect = this.#run(() => {
this.#is_creating_fallback = false; this.#is_creating_fallback = false;
@ -315,16 +376,10 @@ export class Boundary {
if (this.#pending_count > 0) { if (this.#pending_count > 0) {
this.#show_pending_snippet(); this.#show_pending_snippet();
} else { } 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; var previous_reaction = active_reaction;
try { try {
@ -381,18 +436,8 @@ function move_effect(effect, fragment) {
} }
} }
export function get_pending_boundary() { export function get_boundary() {
var boundary = /** @type {Effect} */ (active_effect).b; return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
while (boundary !== null && !boundary.has_pending_snippet()) {
boundary = boundary.parent;
}
if (boundary === null) {
e.await_outside_boundary();
}
return boundary;
} }
export function pending() { export function pending() {

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

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

@ -6,7 +6,7 @@ import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js'; import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants'; 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 { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import { import {
active_effect, active_effect,
@ -65,7 +65,7 @@ export function remove_input_defaults(input) {
// @ts-expect-error // @ts-expect-error
input.__on_r = remove_defaults; input.__on_r = remove_defaults;
queue_idle_task(remove_defaults); queue_micro_task(remove_defaults);
add_form_reset_listener(); 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> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object * @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash] * @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
* @returns {Record<string, any>} * @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 attributes = get_attributes(element);
var is_custom_element = attributes[IS_CUSTOM_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<() => any>} sync
* @param {Array<() => Promise<any>>} async * @param {Array<() => Promise<any>>} async
* @param {string} [css_hash] * @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
*/ */
export function attribute_effect( export function attribute_effect(
@ -475,6 +493,7 @@ export function attribute_effect(
sync = [], sync = [],
async = [], async = [],
css_hash, css_hash,
should_remove_defaults = false,
skip_warning = false skip_warning = false
) { ) {
flatten(sync, async, (values) => { flatten(sync, async, (values) => {
@ -490,7 +509,14 @@ export function attribute_effect(
block(() => { block(() => {
var next = fn(...values.map(get)); var next = fn(...values.map(get));
/** @type {Record<string | symbol, any>} */ /** @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) { if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value); 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 depth = 0;
var node = hydrate_node; var node = hydrate_node;
@ -100,7 +101,7 @@ export function remove_nodes() {
} }
var next = /** @type {TemplateNode} */ (get_next_sibling(node)); var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove(); if (remove) node.remove();
node = next; 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 * Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {DocumentFragment | TemplateNode[]} fragment * @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment
* @param {boolean} is_text * @param {boolean} [is_text]
* @returns {Node | null} * @returns {Node | null}
*/ */
export function first_child(fragment, is_text) { export function first_child(fragment, is_text = false) {
if (!hydrating) { if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment))); var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));

@ -1,60 +1,42 @@
import { run_all } from '../../shared/utils.js'; import { run_all } from '../../shared/utils.js';
import { is_flushing_sync } from '../reactivity/batch.js';
// Fallback for when requestIdleCallback is not available
const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
/** @type {Array<() => void>} */ /** @type {Array<() => void>} */
let micro_tasks = []; let micro_tasks = [];
/** @type {Array<() => void>} */
let idle_tasks = [];
function run_micro_tasks() { function run_micro_tasks() {
var tasks = micro_tasks; var tasks = micro_tasks;
micro_tasks = []; micro_tasks = [];
run_all(tasks); run_all(tasks);
} }
function run_idle_tasks() {
var tasks = idle_tasks;
idle_tasks = [];
run_all(tasks);
}
/** /**
* @param {() => void} fn * @param {() => void} fn
*/ */
export function queue_micro_task(fn) { export function queue_micro_task(fn) {
if (micro_tasks.length === 0) { if (micro_tasks.length === 0 && !is_flushing_sync) {
queueMicrotask(run_micro_tasks); 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); 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. * Synchronously run any queued tasks.
*/ */
export function flush_tasks() { export function flush_tasks() {
if (micro_tasks.length > 0) { while (micro_tasks.length > 0) {
run_micro_tasks(); run_micro_tasks();
} }
if (idle_tasks.length > 0) {
run_idle_tasks();
}
} }

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

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

@ -3,7 +3,7 @@
import { DESTROYED } from '#client/constants'; import { DESTROYED } from '#client/constants';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { component_context, is_runes, set_component_context } from '../context.js'; 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 { invoke_error_boundary } from '../error-handling.js';
import { import {
active_effect, active_effect,
@ -11,7 +11,7 @@ import {
set_active_effect, set_active_effect,
set_active_reaction set_active_reaction
} from '../runtime.js'; } from '../runtime.js';
import { current_batch, suspend } from './batch.js'; import { Batch, current_batch } from './batch.js';
import { import {
async_derived, async_derived,
current_async_effect, current_async_effect,
@ -20,6 +20,14 @@ import {
set_from_async_derived set_from_async_derived
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.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 parent = /** @type {Effect} */ (active_effect);
var restore = capture(); var restore = capture();
var boundary = get_pending_boundary();
var was_hydrating = hydrating;
Promise.all(async.map((expression) => async_derived(expression))) Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => { .then((result) => {
@ -56,11 +65,15 @@ export function flatten(sync, async, fn) {
} }
} }
if (was_hydrating) {
set_hydrating(false);
}
batch?.deactivate(); batch?.deactivate();
unset_context(); unset_context();
}) })
.catch((error) => { .catch((error) => {
boundary.error(error); invoke_error_boundary(error, parent);
}); });
} }
@ -75,12 +88,23 @@ function capture() {
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_batch = current_batch; var previous_batch = current_batch;
var was_hydrating = hydrating;
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
}
return function restore() { return function restore() {
set_active_effect(previous_effect); set_active_effect(previous_effect);
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
set_component_context(previous_component_context); set_component_context(previous_component_context);
previous_batch?.activate(); previous_batch?.activate();
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
if (DEV) { if (DEV) {
set_from_async_derived(null); set_from_async_derived(null);
} }
@ -178,17 +202,52 @@ export function unset_context() {
* @param {() => Promise<void>} fn * @param {() => Promise<void>} fn
*/ */
export async function async_body(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 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 { try {
await fn(); await promise;
} catch (error) { } catch (error) {
if (!aborted(active)) { if (!aborted(active)) {
invoke_error_boundary(error, active); invoke_error_boundary(error, active);
} }
} finally { } 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 { import {
BLOCK_EFFECT, BLOCK_EFFECT,
BRANCH_EFFECT, BRANCH_EFFECT,
@ -10,12 +10,11 @@ import {
INERT, INERT,
RENDER_EFFECT, RENDER_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
USER_EFFECT, MAYBE_DIRTY,
MAYBE_DIRTY DERIVED
} from '#client/constants'; } from '#client/constants';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js'; import { deferred, define_property, noop } from '../../shared/utils.js';
import { get_pending_boundary } from '../dom/blocks/boundary.js';
import { import {
active_effect, active_effect,
is_dirty, is_dirty,
@ -25,12 +24,11 @@ import {
update_effect update_effect
} from '../runtime.js'; } from '../runtime.js';
import * as e from '../errors.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 { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js'; import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js'; import { old_values } from './sources.js';
import { unlink_effect } from './effects.js'; import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
/** @type {Set<Batch>} */ /** @type {Set<Batch>} */
const batches = new Set(); const batches = new Set();
@ -56,19 +54,6 @@ export let batch_deriveds = null;
/** @type {Set<() => void>} */ /** @type {Set<() => void>} */
export let effect_pending_updates = new Set(); 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[]} */ /** @type {Effect[]} */
let queued_root_effects = []; let queued_root_effects = [];
@ -76,7 +61,7 @@ let queued_root_effects = [];
let last_scheduled_effect = null; let last_scheduled_effect = null;
let is_flushing = false; let is_flushing = false;
let is_flushing_sync = false; export let is_flushing_sync = false;
export class Batch { export class Batch {
/** /**
@ -113,22 +98,8 @@ export class Batch {
#deferred = null; #deferred = null;
/** /**
* True if an async effect inside this batch resolved and * Async effects inside a newly-created `<svelte:boundary>`
* its parent branch was already deleted * these do not prevent the batch from committing
*/
#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
* @type {Effect[]} * @type {Effect[]}
*/ */
#boundary_async_effects = []; #boundary_async_effects = [];
@ -181,32 +152,7 @@ export class Batch {
previous_batch = null; previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */ var revert = Batch.apply(this);
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;
}
}
}
}
for (const root of root_effects) { for (const root of root_effects) {
this.#traverse_effect_tree(root); 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 // if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit // is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) { if (this.#pending === 0) {
this.#commit(); this.#commit();
var render_effects = this.#render_effects; 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 // 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. // 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; current_batch = null;
flush_queued_effects(render_effects); flush_queued_effects(render_effects);
flush_queued_effects(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(); this.#deferred?.resolve();
} else { } else {
this.#defer_effects(this.#render_effects); this.#defer_effects(this.#render_effects);
@ -248,27 +185,12 @@ export class Batch {
this.#defer_effects(this.#block_effects); this.#defer_effects(this.#block_effects);
} }
if (current_values) { revert();
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);
}
for (const effect of this.#boundary_async_effects) { for (const effect of this.#boundary_async_effects) {
update_effect(effect); update_effect(effect);
} }
this.#async_effects = [];
this.#boundary_async_effects = []; this.#boundary_async_effects = [];
} }
@ -297,9 +219,8 @@ export class Batch {
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect); this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) { } else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) { if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; this.#boundary_async_effects.push(effect);
effects.push(effect);
} else if (is_dirty(effect)) { } else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect); update_effect(effect);
@ -372,25 +293,17 @@ export class Batch {
} }
} }
neuter() {
this.#neutered = true;
}
flush() { flush() {
if (queued_root_effects.length > 0) { if (queued_root_effects.length > 0) {
this.activate();
flush_effects(); 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
return;
}
if (this.#pending === 0) { if (current_batch !== null && current_batch !== this) {
batches.delete(this); // this can happen if a new batch was created during `flush_effects()`
return;
}
} else if (this.#pending === 0) {
this.#commit();
} }
this.deactivate(); this.deactivate();
@ -400,13 +313,59 @@ export class Batch {
* Append and remove branches to/from the DOM * Append and remove branches to/from the DOM
*/ */
#commit() { #commit() {
if (!this.#neutered) { for (const fn of this.#callbacks) {
for (const fn of this.#callbacks) { fn();
fn();
}
} }
this.#callbacks.clear(); 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() { increment() {
@ -427,9 +386,6 @@ export class Batch {
schedule_effect(e); schedule_effect(e);
} }
this.#render_effects = [];
this.#effects = [];
this.flush(); this.flush();
} else { } else {
this.deactivate(); this.deactivate();
@ -467,11 +423,52 @@ export class Batch {
/** @param {() => void} task */ /** @param {() => void} task */
static enqueue(task) { static enqueue(task) {
if (tasks.length === 0) { queue_micro_task(task);
queueMicrotask(dequeue); }
/**
* @param {Batch} current_batch
*/
static apply(current_batch) {
if (!async_mode_flag || batches.size === 1) {
return noop;
} }
tasks.unshift(task); // 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; var result;
if (fn) { if (fn) {
flush_effects(); if (current_batch !== null) {
flush_effects();
}
result = fn(); result = fn();
} }
@ -641,6 +641,26 @@ function flush_queued_effects(effects) {
eager_block_effects = null; 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 * @param {Effect} signal
* @returns {void} * @returns {void}
@ -667,28 +687,6 @@ export function schedule_effect(signal) {
queued_root_effects.push(effect); 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 * Forcibly remove all current batches, to prevent cross-talk between tests
*/ */

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

@ -11,7 +11,7 @@ import {
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { active_effect } from './runtime.js'; import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.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 { import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
@ -30,7 +30,8 @@ import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { assign_nodes } from './dom/template.js'; import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.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 * This is normally true block effects should run their intro transitions
@ -119,19 +120,9 @@ export function hydrate(component, options) {
set_hydrating(true); set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor)); set_hydrate_node(/** @type {Comment} */ (anchor));
hydrate_next();
const instance = _mount(component, { ...options, anchor }); 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); set_hydrating(false);
return /** @type {Exports} */ (instance); return /** @type {Exports} */ (instance);
@ -152,7 +143,7 @@ export function hydrate(component, options) {
e.hydration_failed(); 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(); init_operations();
clear_text_content(target); clear_text_content(target);
@ -218,35 +209,50 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
var unmount = component_root(() => { var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text()); var anchor_node = anchor ?? target.appendChild(create_text());
branch(() => { boundary(
if (context) { /** @type {TemplateNode} */ (anchor_node),
push({}); {
var ctx = /** @type {ComponentContext} */ (component_context); pending: () => {}
ctx.c = context; },
} (anchor_node) => {
if (context) {
if (events) { push({});
// We can't spread the object or else we'd lose the state proxy stuff, if it is one var ctx = /** @type {ComponentContext} */ (component_context);
/** @type {any} */ (props).$$events = events; ctx.c = context;
} }
if (hydrating) { if (events) {
assign_nodes(/** @type {TemplateNode} */ (anchor_node), null); // We can't spread the object or else we'd lose the state proxy stuff, if it is one
} /** @type {any} */ (props).$$events = events;
}
should_intro = intro; if (hydrating) {
// @ts-expect-error the public typings are not what the actual function looks like assign_nodes(/** @type {TemplateNode} */ (anchor_node), null);
component = Component(anchor_node, props) || {}; }
should_intro = true;
if (hydrating) { should_intro = intro;
/** @type {Effect} */ (active_effect).nodes_end = hydrate_node; // @ts-expect-error the public typings are not what the actual function looks like
} component = Component(anchor_node, props) || {};
should_intro = true;
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) { if (context) {
pop(); pop();
}
} }
}); );
return () => { return () => {
for (var event_name of registered_events) { for (var event_name of registered_events) {
@ -309,7 +315,11 @@ export function unmount(component, options) {
} }
if (DEV) { if (DEV) {
w.lifecycle_double_unmount(); if (STATE_SYMBOL in component) {
w.state_proxy_unmount();
} else {
w.lifecycle_double_unmount();
}
} }
return Promise.resolve(); return Promise.resolve();

@ -500,7 +500,13 @@ export function update_effect(effect) {
*/ */
export async function tick() { export async function tick() {
if (async_mode_flag) { 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(); 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 * A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/ */

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

@ -1,10 +1,14 @@
/** @import { Component } from '#server' */ /** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { on_destroy } from './index.js';
import * as e from './errors.js'; import * as e from './errors.js';
/** @type {Component | null} */ /** @type {SSRContext | null} */
export var current_component = null; export var ssr_context = null;
/** @param {SSRContext | null} v */
export function set_ssr_context(v) {
ssr_context = v;
}
/** /**
* @template T * @template T
@ -47,42 +51,35 @@ export function getAllContexts() {
* @returns {Map<unknown, unknown>} * @returns {Map<unknown, unknown>}
*/ */
function get_or_init_context_map(name) { function get_or_init_context_map(name) {
if (current_component === null) { if (ssr_context === null) {
e.lifecycle_outside_component(name); 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] * @param {Function} [fn]
*/ */
export function push(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) { if (DEV) {
// component function ssr_context.function = fn;
current_component.function = fn; ssr_context.element = ssr_context.p?.element;
} }
} }
export function pop() { export function pop() {
var component = /** @type {Component} */ (current_component); ssr_context = /** @type {SSRContext} */ (ssr_context).p;
var ondestroy = component.d;
if (ondestroy) {
on_destroy.push(...ondestroy);
}
current_component = component.p;
} }
/** /**
* @param {Component} component_context * @param {SSRContext} ssr_context
* @returns {Map<unknown, unknown> | null} * @returns {Map<unknown, unknown> | null}
*/ */
function get_parent_context(component_context) { function get_parent_context(ssr_context) {
let parent = component_context.p; let parent = ssr_context.p;
while (parent !== null) { while (parent !== null) {
const context_map = parent.c; const context_map = parent.c;
@ -94,3 +91,22 @@ function get_parent_context(component_context) {
return null; 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 { FILENAME } from '../../constants.js';
import { import {
is_tag_valid_with_ancestor, is_tag_valid_with_ancestor,
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../html-tree-validation.js'; } 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 * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js'; import { Renderer } from './renderer.js';
// TODO move this
/** /**
* @typedef {{ * @typedef {{
* tag: string; * tag: string;
* parent: null | Element; * parent: undefined | Element;
* filename: null | string; * filename: undefined | string;
* line: number; * line: number;
* column: number; * column: number;
* }} Element * }} Element
*/ */
/** /**
* @type {Element | null} * This is exported so that it can be cleared between tests
* @type {Set<string>}
*/ */
let parent = null; export let seen;
/** @type {Set<string>} */
let seen;
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {string} message * @param {string} message
*/ */
function print_error(payload, message) { function print_error(renderer, message) {
message = message =
`node_invalid_placement_ssr: ${message}\n\n` + `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.'; '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 // eslint-disable-next-line no-console
console.error(message); console.error(message);
payload.head.out.push(`<script>console.error(${JSON.stringify(message)})</script>`); renderer.head((r) => r.push(`<script>console.error(${JSON.stringify(message)})</script>`));
}
export function reset_elements() {
let old_parent = parent;
parent = null;
return () => {
parent = old_parent;
};
} }
/** /**
* @param {Payload} payload * @param {Renderer} renderer
* @param {string} tag * @param {string} tag
* @param {number} line * @param {number} line
* @param {number} column * @param {number} column
*/ */
export function push_element(payload, tag, line, column) { export function push_element(renderer, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME]; var context = /** @type {SSRContext} */ (ssr_context);
var child = { tag, parent, filename, line, column }; 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 ancestor = parent.parent;
var ancestors = [parent.tag]; var ancestors = [parent.tag];
@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) {
: undefined; : undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc); 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) { while (ancestor != null) {
ancestors.push(ancestor.tag); ancestors.push(ancestor.tag);
@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) {
: undefined; : undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc); 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; ancestor = ancestor.parent;
} }
} }
parent = child; set_ssr_context({ ...context, p: context, element });
} }
export function pop_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 ( if (
typeof payload !== 'object' || typeof renderer !== 'object' ||
// for some reason typescript consider the type of payload as never after the first instanceof // for some reason typescript consider the type of renderer as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) !(renderer instanceof Renderer)
) { ) {
e.invalid_snippet_arguments(); e.invalid_snippet_arguments();
} }

@ -2,6 +2,30 @@
export * from '../shared/errors.js'; 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 * `%name%(...)` is not available on the server
* @param {string} name * @param {string} name

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

Loading…
Cancel
Save