diff --git a/.changeset/ninety-olives-report.md b/.changeset/ninety-olives-report.md
deleted file mode 100644
index 3e66a41d02..0000000000
--- a/.changeset/ninety-olives-report.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: replace `undefined` with `void(0)` in CallExpressions
diff --git a/.changeset/wise-schools-report.md b/.changeset/wise-schools-report.md
deleted file mode 100644
index 47ec887256..0000000000
--- a/.changeset/wise-schools-report.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: place store setup inside async body
diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml
index 9be1f00104..aa08df2f84 100644
--- a/.github/workflows/ecosystem-ci-trigger.yml
+++ b/.github/workflows/ecosystem-ci-trigger.yml
@@ -4,6 +4,8 @@ on:
issue_comment:
types: [created]
+permissions: {}
+
jobs:
trigger:
runs-on: ubuntu-latest
@@ -15,7 +17,6 @@ jobs:
contents: read # to clone the repo
steps:
- name: monitor action permissions
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
id: check-permissions
diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml
index 3f1fca5a0b..8bf13bb8f1 100644
--- a/.github/workflows/pkg.pr.new-comment.yml
+++ b/.github/workflows/pkg.pr.new-comment.yml
@@ -14,7 +14,6 @@ jobs:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- - uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
with:
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
index b1ba217e5a..49303f1684 100644
--- a/.github/workflows/pkg.pr.new.yml
+++ b/.github/workflows/pkg.pr.new.yml
@@ -1,6 +1,8 @@
name: Publish Any Commit
on: [push, pull_request]
+permissions: {}
+
jobs:
build:
permissions: {}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6debe5662a..b78346d883 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,6 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- - uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@v4
with:
@@ -27,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 24.x
cache: pnpm
- name: Install
@@ -45,4 +44,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.prettierignore b/.prettierignore
index 5e1d9b1aa7..9cf9a4bfe1 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
+packages/svelte/src/internal/server/warnings.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0653b08b76..e940252892 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved
-There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started:
+There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started:
- Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues).
- Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests).
@@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree
#### Writing tests
-All tests are located in `/test` folder.
+All tests are located in the `/tests` folder.
-Test samples are kept in `/test/xxx/samples` folder.
+Test samples are kept in `/tests/xxx/samples` folders.
#### Running tests
diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md
index e97a46ad34..2ad22c8469 100644
--- a/documentation/docs/01-introduction/02-getting-started.md
+++ b/documentation/docs/01-introduction/02-getting-started.md
@@ -15,11 +15,11 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
-You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
+You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](/packages#routing) as well.
>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
-There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
+There are also [plugins for other bundlers](/packages#bundler-plugins), but we recommend Vite.
## Editor tooling
diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md
index de57815687..c21ed35919 100644
--- a/documentation/docs/03-template-syntax/12-bind.md
+++ b/documentation/docs/03-template-syntax/12-bind.md
@@ -95,7 +95,7 @@ Since 5.6.0, if an ` ` has a `defaultValue` and is part of a form, it will
## ` `
-Checkbox and radio inputs can be bound with `bind:checked`:
+Checkbox inputs can be bound with `bind:checked`:
```svelte
@@ -117,6 +117,8 @@ Since 5.6.0, if an ` ` has a `defaultChecked` attribute and is part of a f
```
+> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
+
## ` `
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:
diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md
index 4e5ec28b26..1c613af870 100644
--- a/documentation/docs/03-template-syntax/19-await-expressions.md
+++ b/documentation/docs/03-template-syntax/19-await-expressions.md
@@ -23,24 +23,6 @@ export default {
The experimental flag will be removed in Svelte 6.
-## Boundaries
-
-Currently, you can only use `await` inside a [``](svelte-boundary) with a `pending` snippet:
-
-```svelte
-
-
-
- {#snippet pending()}
- loading...
- {/snippet}
-
-```
-
-This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
-
-> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
-
## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
@@ -99,7 +81,9 @@ let b = $derived(await two());
## Indicating loading states
-In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
+To render placeholder UI, you can wrap content in a `` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
+
+After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
@@ -133,12 +117,28 @@ async function onclick() {
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
+## Server-side rendering
+
+Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
+
+```js
+/// file: server.js
+import { render } from 'svelte/server';
+import App from './App.svelte';
+
+const { head, body } = +++await+++ render(App);
+```
+
+> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
+
+If a `` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
+
+> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
+
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
-Currently, server-side rendering is synchronous. If a `` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
-
## Breaking changes
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).
diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md
index bcec4db0a3..4807fc8f0c 100644
--- a/documentation/docs/07-misc/02-testing.md
+++ b/documentation/docs/07-misc/02-testing.md
@@ -4,7 +4,7 @@ title: Testing
Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like [Vitest](https://vitest.dev/), [Jasmine](https://jasmine.github.io/), [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/).
-## Unit and integration testing using Vitest
+## Unit and component tests with Vitest
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
@@ -160,9 +160,9 @@ export function logger(getValue) {
### Component testing
-It is possible to test your components in isolation using Vitest.
+It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
-> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component
+> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component.
To get started, install jsdom (a library that shims DOM APIs):
@@ -246,7 +246,49 @@ test('Component', async () => {
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
-## E2E tests using Playwright
+## Component tests with Storybook
+
+[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
+
+To get started, first install Storybook ([using Svelte's CLI](/docs/cli/storybook)) in your project via `npx sv add storybook` and choose the recommended configuration that includes testing features. If you're already using Storybook, and for more information on Storybook's testing capabilities, follow the [Storybook testing docs](https://storybook.js.org/docs/writing-tests?renderer=svelte) to get started.
+
+You can create stories for component variations and test interactions with the [play function](https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=svelte#writing-interaction-tests), which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here's an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
+
+```svelte
+/// file: LoginForm.stories.svelte
+
+
+
+
+ {
+ // 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('You’re 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/).
diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md
index cf98cdd3c3..1816998299 100644
--- a/documentation/docs/07-misc/99-faq.md
+++ b/documentation/docs/07-misc/99-faq.md
@@ -65,7 +65,7 @@ There will be a blog post about this eventually, but in the meantime, check out
## Is there a UI component library?
-There are several UI component libraries as well as standalone components. Find them under the [design systems section of the components page](https://sveltesociety.dev/packages?category=design-system) on the Svelte Society website.
+There are several [UI component libraries](/packages#component-libraries) as well as standalone components listed on [the packages page](/packages).
## How do I test Svelte apps?
@@ -91,17 +91,9 @@ Some resources for getting started with testing:
## Is there a router?
-The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React.
+The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React and Nuxt.js for Vue.
-However, you can use any router library. A lot of people use [page.js](https://github.com/visionmedia/page.js). There's also [navaid](https://github.com/lukeed/navaid), which is very similar. And [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic with child routes, but without built-in history support.
-
-If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
-
-If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
-
-[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
-
-You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers).
+However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).
## How do I write a mobile app with Svelte?
diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md
index 6f1d677fe9..c95ace2229 100644
--- a/documentation/docs/98-reference/.generated/client-warnings.md
+++ b/documentation/docs/98-reference/.generated/client-warnings.md
@@ -312,6 +312,27 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
+### state_proxy_unmount
+
+```
+Tried to unmount a state proxy, rather than a component
+```
+
+`unmount` was called with a state proxy:
+
+```js
+import { mount, unmount } from 'svelte';
+import Component from './Component.svelte';
+let target = document.body;
+// ---cut---
+let component = $state(mount(Component, { target }));
+
+// later...
+unmount(component);
+```
+
+Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
+
### svelte_boundary_reset_noop
```
diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md
index 17841b863c..7dfbe75888 100644
--- a/documentation/docs/98-reference/.generated/compile-warnings.md
+++ b/documentation/docs/98-reference/.generated/compile-warnings.md
@@ -81,7 +81,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
### a11y_consider_explicit_label
```
-Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
+Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
```
### a11y_distracting_elements
diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md
index c3e8b53c31..6263032212 100644
--- a/documentation/docs/98-reference/.generated/server-errors.md
+++ b/documentation/docs/98-reference/.generated/server-errors.md
@@ -1,5 +1,19 @@
+### 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) with a `pending` snippet.
+
+### html_deprecated
+
+```
+The `html` property of server render results has been deprecated. Use `body` instead.
+```
+
### lifecycle_function_unavailable
```
diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md
new file mode 100644
index 0000000000..26b3628be9
--- /dev/null
+++ b/documentation/docs/98-reference/.generated/server-warnings.md
@@ -0,0 +1,9 @@
+
+
+### 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.
diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md
index de34b3f5da..6c31aaafd0 100644
--- a/documentation/docs/98-reference/.generated/shared-errors.md
+++ b/documentation/docs/98-reference/.generated/shared-errors.md
@@ -1,25 +1,5 @@
-### await_outside_boundary
-
-```
-Cannot await outside a `` 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 ``);
-}
-
-export function reset_elements() {
- let old_parent = parent;
- parent = null;
- return () => {
- parent = old_parent;
- };
+ renderer.head((r) => r.push(``));
}
/**
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @param {string} tag
* @param {number} line
* @param {number} column
*/
-export function push_element(payload, tag, line, column) {
- var filename = /** @type {Component} */ (current_component).function[FILENAME];
- var child = { tag, parent, filename, line, column };
+export function push_element(renderer, tag, line, column) {
+ var context = /** @type {SSRContext} */ (ssr_context);
+ var filename = context.function[FILENAME];
+ var parent = context.element;
+ var element = { tag, parent, filename, line, column };
- if (parent !== null) {
+ if (parent !== undefined) {
var ancestor = parent.parent;
var ancestors = [parent.tag];
@@ -71,7 +64,7 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
- if (message) print_error(payload, message);
+ if (message) print_error(renderer, message);
while (ancestor != null) {
ancestors.push(ancestor.tag);
@@ -80,27 +73,27 @@ export function push_element(payload, tag, line, column) {
: undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
- if (message) print_error(payload, message);
+ if (message) print_error(renderer, message);
ancestor = ancestor.parent;
}
}
- parent = child;
+ set_ssr_context({ ...context, p: context, element });
}
export function pop_element() {
- parent = /** @type {Element} */ (parent).parent;
+ set_ssr_context(/** @type {SSRContext} */ (ssr_context).p);
}
/**
- * @param {Payload} payload
+ * @param {Renderer} renderer
*/
-export function validate_snippet_args(payload) {
+export function validate_snippet_args(renderer) {
if (
- typeof payload !== 'object' ||
- // for some reason typescript consider the type of payload as never after the first instanceof
- !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
+ typeof renderer !== 'object' ||
+ // for some reason typescript consider the type of renderer as never after the first instanceof
+ !(renderer instanceof Renderer)
) {
e.invalid_snippet_arguments();
}
diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js
index 458937218f..bde49fe935 100644
--- a/packages/svelte/src/internal/server/errors.js
+++ b/packages/svelte/src/internal/server/errors.js
@@ -2,6 +2,30 @@
export * from '../shared/errors.js';
+/**
+ * Encountered asynchronous work while rendering synchronously.
+ * @returns {never}
+ */
+export function await_invalid() {
+ const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+}
+
+/**
+ * The `html` property of server render results has been deprecated. Use `body` instead.
+ * @returns {never}
+ */
+export function html_deprecated() {
+ const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+}
+
/**
* `%name%(...)` is not available on the server
* @param {string} name
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 62ee22d6fc..50bb629c4d 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -1,6 +1,7 @@
-/** @import { ComponentType, SvelteComponent } from 'svelte' */
-/** @import { Component, RenderOutput } from '#server' */
+/** @import { ComponentType, SvelteComponent, Component } from 'svelte' */
+/** @import { RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
+/** @import { AccumulatedContent } from './renderer.js' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
@@ -8,17 +9,15 @@ import { subscribe_to_store } from '../../store/utils.js';
import {
UNINITIALIZED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
- ELEMENT_IS_NAMESPACED
+ ELEMENT_IS_NAMESPACED,
+ ELEMENT_IS_INPUT
} from '../../constants.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
-import { current_component, pop, push } from './context.js';
import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
-import { reset_elements } from './dev.js';
-import { Payload } from './payload.js';
-import { abort } from './abort-signal.js';
+import { Renderer } from './renderer.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@@ -26,150 +25,96 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
/**
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @param {string} tag
* @param {() => void} attributes_fn
* @param {() => void} children_fn
* @returns {void}
*/
-export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
- payload.out.push('');
+export function element(renderer, tag, attributes_fn = noop, children_fn = noop) {
+ renderer.push('');
if (tag) {
- payload.out.push(`<${tag}`);
+ renderer.push(`<${tag}`);
attributes_fn();
- payload.out.push(`>`);
+ renderer.push(`>`);
if (!is_void(tag)) {
children_fn();
if (!is_raw_text_element(tag)) {
- payload.out.push(EMPTY_COMMENT);
+ renderer.push(EMPTY_COMMENT);
}
- payload.out.push(`${tag}>`);
+ renderer.push(`${tag}>`);
}
}
- payload.out.push('');
+ renderer.push('');
}
-/**
- * Array of `onDestroy` callbacks that should be called at the end of the server render function
- * @type {Function[]}
- */
-export let on_destroy = [];
-
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record} Props
- * @param {import('svelte').Component | ComponentType>} component
+ * @param {Component | ComponentType>} component
* @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
- try {
- const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
-
- const prev_on_destroy = on_destroy;
- on_destroy = [];
- payload.out.push(BLOCK_OPEN);
-
- let reset_reset_element;
-
- if (DEV) {
- // prevent parent/child element state being corrupted by a bad render
- reset_reset_element = reset_elements();
- }
-
- if (options.context) {
- push();
- /** @type {Component} */ (current_component).c = options.context;
- }
-
- // @ts-expect-error
- component(payload, options.props ?? {}, {}, {});
-
- if (options.context) {
- pop();
- }
-
- if (reset_reset_element) {
- reset_reset_element();
- }
-
- payload.out.push(BLOCK_CLOSE);
- for (const cleanup of on_destroy) cleanup();
- on_destroy = prev_on_destroy;
-
- let head = payload.head.out.join('') + payload.head.title;
-
- for (const { hash, code } of payload.css) {
- head += ``;
- }
-
- const body = payload.out.join('');
-
- return {
- head,
- html: body,
- body: body
- };
- } finally {
- abort();
- }
+ return Renderer.render(/** @type {Component} */ (component), options);
}
/**
- * @param {Payload} payload
- * @param {(head_payload: Payload['head']) => void} fn
+ * @param {Renderer} renderer
+ * @param {(renderer: Renderer) => Promise | void} fn
* @returns {void}
*/
-export function head(payload, fn) {
- const head_payload = payload.head;
- head_payload.out.push(BLOCK_OPEN);
- fn(head_payload);
- head_payload.out.push(BLOCK_CLOSE);
+export function head(renderer, fn) {
+ renderer.head((renderer) => {
+ renderer.push(BLOCK_OPEN);
+ renderer.child(fn);
+ renderer.push(BLOCK_CLOSE);
+ });
}
/**
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @param {boolean} is_html
* @param {Record} props
* @param {() => void} component
* @param {boolean} dynamic
* @returns {void}
*/
-export function css_props(payload, is_html, props, component, dynamic = false) {
+export function css_props(renderer, is_html, props, component, dynamic = false) {
const styles = style_object_to_string(props);
if (is_html) {
- payload.out.push(``);
+ renderer.push(``);
} else {
- payload.out.push(``);
+ renderer.push(``);
}
if (dynamic) {
- payload.out.push('');
+ renderer.push('');
}
component();
if (is_html) {
- payload.out.push(` `);
+ renderer.push(` `);
} else {
- payload.out.push(``);
+ renderer.push(``);
}
}
/**
* @param {Record} attrs
- * @param {string | null} css_hash
+ * @param {string} [css_hash]
* @param {Record} [classes]
* @param {Record} [styles]
* @param {number} [flags]
* @returns {string}
*/
-export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
+export function attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
attrs.style = to_style(attrs.style, styles);
}
@@ -187,6 +132,7 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0;
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0;
+ const is_input = (flags & ELEMENT_IS_INPUT) !== 0;
for (name in attrs) {
// omit functions, internal svelte properties and invalid attribute names
@@ -200,6 +146,13 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
name = name.toLowerCase();
}
+ if (is_input) {
+ if (name === 'defaultvalue' || name === 'defaultchecked') {
+ name = name === 'defaultvalue' ? 'value' : 'checked';
+ if (attrs[name]) continue;
+ }
+ }
+
attr_str += attr(name, value, is_html && is_boolean_attribute(name));
}
@@ -351,14 +304,14 @@ export function unsubscribe_stores(store_values) {
}
/**
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @param {Record} $$props
* @param {string} name
* @param {Record} slot_props
* @param {null | (() => void)} fallback_fn
* @returns {void}
*/
-export function slot(payload, $$props, name, slot_props, fallback_fn) {
+export function slot(renderer, $$props, name, slot_props, fallback_fn) {
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
if (slot_fn === true) {
@@ -366,7 +319,7 @@ export function slot(payload, $$props, name, slot_props, fallback_fn) {
}
if (slot_fn !== undefined) {
- slot_fn(payload, slot_props);
+ slot_fn(renderer, slot_props);
} else {
fallback_fn?.();
}
@@ -434,21 +387,21 @@ export function bind_props(props_parent, props_now) {
/**
* @template V
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @param {Promise} promise
* @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn
* @returns {void}
*/
-function await_block(payload, promise, pending_fn, then_fn) {
+function await_block(renderer, promise, pending_fn, then_fn) {
if (is_promise(promise)) {
- payload.out.push(BLOCK_OPEN);
+ renderer.push(BLOCK_OPEN);
promise.then(null, noop);
if (pending_fn !== null) {
pending_fn();
}
} else if (then_fn !== null) {
- payload.out.push(BLOCK_OPEN_ELSE);
+ renderer.push(BLOCK_OPEN_ELSE);
then_fn(promise);
}
}
@@ -490,12 +443,12 @@ export function once(get_value) {
/**
* Create an unique ID
- * @param {Payload} payload
+ * @param {Renderer} renderer
* @returns {string}
*/
-export function props_id(payload) {
- const uid = payload.uid();
- payload.out.push('');
+export function props_id(renderer) {
+ const uid = renderer.global.uid();
+ renderer.push('');
return uid;
}
@@ -503,12 +456,10 @@ export { attr, clsx };
export { html } from './blocks/html.js';
-export { push, pop } from './context.js';
+export { save } from './context.js';
export { push_element, pop_element, validate_snippet_args } from './dev.js';
-export { assign_payload, copy_payload } from './payload.js';
-
export { snapshot } from '../shared/clone.js';
export { fallback, to_array } from '../shared/utils.js';
@@ -522,8 +473,6 @@ export {
export { escape_html as escape };
-export { await_outside_boundary } from '../shared/errors.js';
-
/**
* @template T
* @param {()=>T} fn
@@ -544,33 +493,3 @@ export function derived(fn) {
return updated_value;
};
}
-
-/**
- *
- * @param {Payload} payload
- * @param {*} value
- */
-export function maybe_selected(payload, value) {
- return value === payload.select_value ? ' selected' : '';
-}
-
-/**
- * @param {Payload} payload
- * @param {() => void} children
- * @returns {void}
- */
-export function valueless_option(payload, children) {
- var i = payload.out.length;
-
- children();
-
- var body = payload.out.slice(i).join('');
-
- if (body.replace(//g, '') === payload.select_value) {
- // replace '>' with ' selected>' (closing tag will be added later)
- var last_item = payload.out[i - 1];
- payload.out[i - 1] = last_item.slice(0, -1) + ' selected>';
- // Remove the old items after position i and add the body as a single item
- payload.out.splice(i, payload.out.length - i, body);
- }
-}
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
deleted file mode 100644
index a7e40ad1db..0000000000
--- a/packages/svelte/src/internal/server/payload.js
+++ /dev/null
@@ -1,80 +0,0 @@
-export class HeadPayload {
- /** @type {Set<{ hash: string; code: string }>} */
- css = new Set();
- /** @type {string[]} */
- out = [];
- uid = () => '';
- title = '';
-
- constructor(
- /** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
- /** @type {string[]} */ out = [],
- title = '',
- uid = () => ''
- ) {
- this.css = css;
- this.out = out;
- this.title = title;
- this.uid = uid;
- }
-}
-
-export class Payload {
- /** @type {Set<{ hash: string; code: string }>} */
- css = new Set();
- /** @type {string[]} */
- out = [];
- uid = () => '';
- select_value = undefined;
-
- head = new HeadPayload();
-
- constructor(id_prefix = '') {
- this.uid = props_id_generator(id_prefix);
- this.head.uid = this.uid;
- }
-}
-
-/**
- * Used in legacy mode to handle bindings
- * @param {Payload} to_copy
- * @returns {Payload}
- */
-export function copy_payload({ out, css, head, uid }) {
- const payload = new Payload();
-
- payload.out = [...out];
- payload.css = new Set(css);
- payload.uid = uid;
-
- payload.head = new HeadPayload();
- payload.head.out = [...head.out];
- payload.head.css = new Set(head.css);
- payload.head.title = head.title;
- payload.head.uid = head.uid;
-
- return payload;
-}
-
-/**
- * Assigns second payload to first
- * @param {Payload} p1
- * @param {Payload} p2
- * @returns {void}
- */
-export function assign_payload(p1, p2) {
- p1.out = [...p2.out];
- p1.css = p2.css;
- p1.head = p2.head;
- p1.uid = p2.uid;
-}
-
-/**
- * Creates an ID generator
- * @param {string} prefix
- * @returns {() => string}
- */
-function props_id_generator(prefix) {
- let uid = 1;
- return () => `${prefix}s${uid++}`;
-}
diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js
new file mode 100644
index 0000000000..bbb43a6f3b
--- /dev/null
+++ b/packages/svelte/src/internal/server/renderer.js
@@ -0,0 +1,622 @@
+/** @import { Component } from 'svelte' */
+/** @import { RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
+import { async_mode_flag } from '../flags/index.js';
+import { abort } from './abort-signal.js';
+import { pop, push, set_ssr_context, ssr_context } from './context.js';
+import * as e from './errors.js';
+import * as w from './warnings.js';
+import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
+import { attributes } from './index.js';
+
+/** @typedef {'head' | 'body'} RendererType */
+/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
+/**
+ * @template T
+ * @typedef {T | Promise} MaybePromise
+ */
+/**
+ * @typedef {string | Renderer} RendererItem
+ */
+
+/**
+ * Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
+ * work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the
+ * content from itself and all of its children, but this will throw if any of the children are
+ * performing asynchronous work. To asynchronously collect a renderer, just `await` it.
+ *
+ * The `string` values within a renderer are always associated with the {@link type} of that renderer. To switch types,
+ * call {@link child} with a different `type` argument.
+ */
+export class Renderer {
+ /**
+ * The contents of the renderer.
+ * @type {RendererItem[]}
+ */
+ #out = [];
+
+ /**
+ * Any `onDestroy` callbacks registered during execution of this renderer.
+ * @type {(() => void)[] | undefined}
+ */
+ #on_destroy = undefined;
+
+ /**
+ * Whether this renderer is a component body.
+ * @type {boolean}
+ */
+ #is_component_body = false;
+
+ /**
+ * The type of string content that this renderer is accumulating.
+ * @type {RendererType}
+ */
+ type;
+
+ /** @type {Renderer | undefined} */
+ #parent;
+
+ /**
+ * Asynchronous work associated with this renderer
+ * @type {Promise | undefined}
+ */
+ promise = undefined;
+
+ /**
+ * State which is associated with the content tree as a whole.
+ * It will be re-exposed, uncopied, on all children.
+ * @type {SSRState}
+ * @readonly
+ */
+ global;
+
+ /**
+ * State that is local to the branch it is declared in.
+ * It will be shallow-copied to all children.
+ *
+ * @type {{ select_value: string | undefined }}
+ */
+ local;
+
+ /**
+ * @param {SSRState} global
+ * @param {Renderer | undefined} [parent]
+ */
+ constructor(global, parent) {
+ this.#parent = parent;
+
+ this.global = global;
+ this.local = parent ? { ...parent.local } : { select_value: undefined };
+ this.type = parent ? parent.type : 'body';
+ }
+
+ /**
+ * @param {(renderer: Renderer) => void} fn
+ */
+ head(fn) {
+ const head = new Renderer(this.global, this);
+ head.type = 'head';
+
+ this.#out.push(head);
+ head.child(fn);
+ }
+
+ /**
+ * @param {(renderer: Renderer) => void} fn
+ */
+ async(fn) {
+ this.#out.push(BLOCK_OPEN);
+ this.child(fn);
+ this.#out.push(BLOCK_CLOSE);
+ }
+
+ /**
+ * Create a child renderer. The child renderer inherits the state from the parent,
+ * but has its own content.
+ * @param {(renderer: Renderer) => MaybePromise} fn
+ */
+ child(fn) {
+ const child = new Renderer(this.global, this);
+ this.#out.push(child);
+
+ const parent = ssr_context;
+
+ set_ssr_context({
+ ...ssr_context,
+ p: parent,
+ c: null,
+ r: child
+ });
+
+ const result = fn(child);
+
+ set_ssr_context(parent);
+
+ if (result instanceof Promise) {
+ if (child.global.mode === 'sync') {
+ e.await_invalid();
+ }
+ // just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails
+ result.catch(() => {});
+ child.promise = result;
+ }
+
+ return child;
+ }
+
+ /**
+ * Create a component renderer. The component renderer inherits the state from the parent,
+ * but has its own content. It is treated as an ordering boundary for ondestroy callbacks.
+ * @param {(renderer: Renderer) => MaybePromise} fn
+ * @param {Function} [component_fn]
+ * @returns {void}
+ */
+ component(fn, component_fn) {
+ push(component_fn);
+ const child = this.child(fn);
+ child.#is_component_body = true;
+ pop();
+ }
+
+ /**
+ * @param {Record} attrs
+ * @param {(renderer: Renderer) => void} fn
+ * @param {string | undefined} [css_hash]
+ * @param {Record | undefined} [classes]
+ * @param {Record | undefined} [styles]
+ * @param {number | undefined} [flags]
+ * @returns {void}
+ */
+ select(attrs, fn, css_hash, classes, styles, flags) {
+ const { value, ...select_attrs } = attrs;
+
+ this.push(``);
+ this.child((renderer) => {
+ renderer.local.select_value = value;
+ fn(renderer);
+ });
+ this.push(' ');
+ }
+
+ /**
+ * @param {Record} attrs
+ * @param {string | number | boolean | ((renderer: Renderer) => void)} body
+ * @param {string | undefined} [css_hash]
+ * @param {Record | undefined} [classes]
+ * @param {Record | undefined} [styles]
+ * @param {number | undefined} [flags]
+ */
+ option(attrs, body, css_hash, classes, styles, flags) {
+ this.#out.push(` {
+ if ('value' in attrs) {
+ value = attrs.value;
+ }
+
+ if (value === this.local.select_value) {
+ renderer.#out.push(' selected');
+ }
+
+ renderer.#out.push(`>${body} `);
+
+ // super edge case, but may as well handle it
+ if (head) {
+ renderer.head((child) => child.push(head));
+ }
+ };
+
+ if (typeof body === 'function') {
+ this.child((renderer) => {
+ const r = new Renderer(this.global, this);
+ body(r);
+
+ if (this.global.mode === 'async') {
+ return r.#collect_content_async().then((content) => {
+ close(renderer, content.body.replaceAll('', ''), content);
+ });
+ } else {
+ const content = r.#collect_content();
+ close(renderer, content.body.replaceAll('', ''), content);
+ }
+ });
+ } else {
+ close(this, body, { body });
+ }
+ }
+
+ /**
+ * @param {(renderer: Renderer) => void} fn
+ */
+ title(fn) {
+ const path = this.get_path();
+
+ /** @param {string} head */
+ const close = (head) => {
+ this.global.set_title(head, path);
+ };
+
+ this.child((renderer) => {
+ const r = new Renderer(renderer.global, renderer);
+ fn(r);
+
+ if (renderer.global.mode === 'async') {
+ return r.#collect_content_async().then((content) => {
+ close(content.head);
+ });
+ } else {
+ const content = r.#collect_content();
+ close(content.head);
+ }
+ });
+ }
+
+ /**
+ * @param {string | (() => Promise)} content
+ */
+ push(content) {
+ if (typeof content === 'function') {
+ this.child(async (renderer) => renderer.push(await content()));
+ } else {
+ this.#out.push(content);
+ }
+ }
+
+ /**
+ * @param {() => void} fn
+ */
+ on_destroy(fn) {
+ (this.#on_destroy ??= []).push(fn);
+ }
+
+ /**
+ * @returns {number[]}
+ */
+ get_path() {
+ return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : [];
+ }
+
+ /**
+ * @deprecated this is needed for legacy component bindings
+ */
+ copy() {
+ const copy = new Renderer(this.global, this.#parent);
+ copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item));
+ copy.promise = this.promise;
+ return copy;
+ }
+
+ /**
+ * @param {Renderer} other
+ * @deprecated this is needed for legacy component bindings
+ */
+ subsume(other) {
+ if (this.global.mode !== other.global.mode) {
+ throw new Error(
+ "invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
+ );
+ }
+
+ this.local = other.local;
+ this.#out = other.#out.map((item) => {
+ if (item instanceof Renderer) {
+ item.subsume(item);
+ }
+ return item;
+ });
+ this.promise = other.promise;
+ this.type = other.type;
+ }
+
+ get length() {
+ return this.#out.length;
+ }
+
+ /**
+ * Only available on the server and when compiling with the `server` option.
+ * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
+ * @template {Record} Props
+ * @param {Component} component
+ * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options]
+ * @returns {RenderOutput}
+ */
+ static render(component, options = {}) {
+ /** @type {AccumulatedContent | undefined} */
+ let sync;
+ /** @type {Promise | undefined} */
+ let async;
+
+ const result = /** @type {RenderOutput} */ ({});
+ // making these properties non-enumerable so that console.logging
+ // doesn't trigger a sync render
+ Object.defineProperties(result, {
+ html: {
+ get: () => {
+ return (sync ??= Renderer.#render(component, options)).body;
+ }
+ },
+ head: {
+ get: () => {
+ return (sync ??= Renderer.#render(component, options)).head;
+ }
+ },
+ body: {
+ get: () => {
+ return (sync ??= Renderer.#render(component, options)).body;
+ }
+ },
+ then: {
+ value:
+ /**
+ * this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
+ *
+ * @template TResult1
+ * @template [TResult2=never]
+ * @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
+ * @param { (reason: unknown) => TResult2 } onrejected
+ */
+ (onfulfilled, onrejected) => {
+ if (!async_mode_flag) {
+ w.experimental_async_ssr();
+ const result = (sync ??= Renderer.#render(component, options));
+ const user_result = onfulfilled({
+ head: result.head,
+ body: result.body,
+ html: result.body
+ });
+ return Promise.resolve(user_result);
+ }
+ async ??= Renderer.#render_async(component, options);
+ return async.then((result) => {
+ Object.defineProperty(result, 'html', {
+ // eslint-disable-next-line getter-return
+ get: () => {
+ e.html_deprecated();
+ }
+ });
+ return onfulfilled(/** @type {SyncRenderOutput} */ (result));
+ }, onrejected);
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call
+ * after awaiting `collect_async`.
+ *
+ * Child renderers are "porous" and don't affect execution order, but component body renderers
+ * create ordering boundaries. Within a renderer, callbacks run in order until hitting a component boundary.
+ * @returns {Iterable<() => void>}
+ */
+ *#collect_on_destroy() {
+ for (const component of this.#traverse_components()) {
+ yield* component.#collect_ondestroy();
+ }
+ }
+
+ /**
+ * Performs a depth-first search of renderers, yielding the deepest components first, then additional components as we backtrack up the tree.
+ * @returns {Iterable}
+ */
+ *#traverse_components() {
+ for (const child of this.#out) {
+ if (typeof child !== 'string') {
+ yield* child.#traverse_components();
+ }
+ }
+ if (this.#is_component_body) {
+ yield this;
+ }
+ }
+
+ /**
+ * @returns {Iterable<() => void>}
+ */
+ *#collect_ondestroy() {
+ if (this.#on_destroy) {
+ for (const fn of this.#on_destroy) {
+ yield fn;
+ }
+ }
+ for (const child of this.#out) {
+ if (child instanceof Renderer && !child.#is_component_body) {
+ yield* child.#collect_ondestroy();
+ }
+ }
+ }
+
+ /**
+ * Render a component. Throws if any of the children are performing asynchronous work.
+ *
+ * @template {Record} Props
+ * @param {Component} component
+ * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options
+ * @returns {AccumulatedContent}
+ */
+ static #render(component, options) {
+ var previous_context = ssr_context;
+ try {
+ const renderer = Renderer.#open_render('sync', component, options);
+
+ const content = renderer.#collect_content();
+ return Renderer.#close_render(content, renderer);
+ } finally {
+ abort();
+ set_ssr_context(previous_context);
+ }
+ }
+
+ /**
+ * Render a component.
+ *
+ * @template {Record} Props
+ * @param {Component} component
+ * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options
+ * @returns {Promise}
+ */
+ static async #render_async(component, options) {
+ var previous_context = ssr_context;
+ try {
+ const renderer = Renderer.#open_render('async', component, options);
+
+ const content = await renderer.#collect_content_async();
+ return Renderer.#close_render(content, renderer);
+ } finally {
+ abort();
+ set_ssr_context(previous_context);
+ }
+ }
+
+ /**
+ * Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
+ * @param {AccumulatedContent} content
+ * @returns {AccumulatedContent}
+ */
+ #collect_content(content = { head: '', body: '' }) {
+ for (const item of this.#out) {
+ if (typeof item === 'string') {
+ content[this.type] += item;
+ } else if (item instanceof Renderer) {
+ item.#collect_content(content);
+ }
+ }
+
+ return content;
+ }
+
+ /**
+ * Collect all of the code from the `out` array and return it as a string.
+ * @param {AccumulatedContent} content
+ * @returns {Promise}
+ */
+ async #collect_content_async(content = { head: '', body: '' }) {
+ await this.promise;
+
+ // no danger to sequentially awaiting stuff in here; all of the work is already kicked off
+ for (const item of this.#out) {
+ if (typeof item === 'string') {
+ content[this.type] += item;
+ } else if (item instanceof Renderer) {
+ await item.#collect_content_async(content);
+ }
+ }
+
+ return content;
+ }
+
+ /**
+ * @template {Record} Props
+ * @param {'sync' | 'async'} mode
+ * @param {import('svelte').Component} component
+ * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options
+ * @returns {Renderer}
+ */
+ static #open_render(mode, component, options) {
+ const renderer = new Renderer(
+ new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '')
+ );
+
+ renderer.push(BLOCK_OPEN);
+
+ if (options.context) {
+ push();
+ /** @type {SSRContext} */ (ssr_context).c = options.context;
+ /** @type {SSRContext} */ (ssr_context).r = renderer;
+ }
+
+ // @ts-expect-error
+ component(renderer, options.props ?? {});
+
+ if (options.context) {
+ pop();
+ }
+
+ renderer.push(BLOCK_CLOSE);
+
+ return renderer;
+ }
+
+ /**
+ * @param {AccumulatedContent} content
+ * @param {Renderer} renderer
+ */
+ static #close_render(content, renderer) {
+ for (const cleanup of renderer.#collect_on_destroy()) {
+ cleanup();
+ }
+
+ let head = content.head + renderer.global.get_title();
+ let body = content.body;
+
+ for (const { hash, code } of renderer.global.css) {
+ head += ``;
+ }
+
+ return {
+ head,
+ body
+ };
+ }
+}
+
+export class SSRState {
+ /** @readonly @type {'sync' | 'async'} */
+ mode;
+
+ /** @readonly @type {() => string} */
+ uid;
+
+ /** @readonly @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+
+ /** @type {{ path: number[], value: string }} */
+ #title = { path: [], value: '' };
+
+ /**
+ * @param {'sync' | 'async'} mode
+ * @param {string} [id_prefix]
+ */
+ constructor(mode, id_prefix = '') {
+ this.mode = mode;
+
+ let uid = 1;
+ this.uid = () => `${id_prefix}s${uid++}`;
+ }
+
+ get_title() {
+ return this.#title.value;
+ }
+
+ /**
+ * Performs a depth-first (lexicographic) comparison using the path. Rejects sets
+ * from earlier than or equal to the current value.
+ * @param {string} value
+ * @param {number[]} path
+ */
+ set_title(value, path) {
+ const current = this.#title.path;
+
+ let i = 0;
+ let l = Math.min(path.length, current.length);
+
+ // skip identical prefixes - [1, 2, 3, ...] === [1, 2, 3, ...]
+ while (i < l && path[i] === current[i]) i += 1;
+
+ if (path[i] === undefined) return;
+
+ // replace title if
+ // - incoming path is longer - [7, 8, 9] > [7, 8]
+ // - incoming path is later - [7, 8, 9] > [7, 8, 8]
+ if (current[i] === undefined || path[i] > current[i]) {
+ this.#title.path = path;
+ this.#title.value = value;
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/server/renderer.test.ts b/packages/svelte/src/internal/server/renderer.test.ts
new file mode 100644
index 0000000000..8e9a377a5b
--- /dev/null
+++ b/packages/svelte/src/internal/server/renderer.test.ts
@@ -0,0 +1,357 @@
+import { afterAll, beforeAll, describe, expect, test } from 'vitest';
+import { Renderer, SSRState } from './renderer.js';
+import type { Component } from 'svelte';
+import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
+
+test('collects synchronous body content by default', () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('a');
+ renderer.child(($$renderer) => {
+ $$renderer.push('b');
+ });
+ renderer.push('c');
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe('abc');
+});
+
+test('child type switches content area (head vs body)', () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('a');
+ renderer.head(($$renderer) => {
+ $$renderer.push('T ');
+ });
+ renderer.push('b');
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(head).toBe('T ');
+ expect(body).toBe('ab');
+});
+
+test('child inherits parent type when not specified', () => {
+ const component = (renderer: Renderer) => {
+ renderer.head((renderer) => {
+ renderer.push(' ');
+ renderer.child((renderer) => {
+ renderer.push('');
+ });
+ });
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(body).toBe('');
+ expect(head).toBe(' ');
+});
+
+test('get_path returns the path indexes to a renderer', () => {
+ const root = new Renderer(new SSRState('sync'));
+ let child_a: InstanceType | undefined;
+ let child_b: InstanceType | undefined;
+ let child_b_0: InstanceType | undefined;
+
+ root.child(($$renderer) => {
+ child_a = $$renderer;
+ $$renderer.push('A');
+ });
+ root.child(($$renderer) => {
+ child_b = $$renderer;
+ $$renderer.child(($$inner) => {
+ child_b_0 = $$inner;
+ $$inner.push('B0');
+ });
+ $$renderer.push('B1');
+ });
+
+ expect(child_a!.get_path()).toEqual([0]);
+ expect(child_b!.get_path()).toEqual([1]);
+ expect(child_b_0!.get_path()).toEqual([1, 0]);
+});
+
+test('creating an async child in a sync context throws', () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('a');
+ renderer.child(async ($$renderer) => {
+ await Promise.resolve();
+ $$renderer.push('x');
+ });
+ };
+
+ expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid');
+ expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid');
+ expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid');
+});
+
+test('local state is shallow-copied to children', () => {
+ const root = new Renderer(new SSRState('sync'));
+ root.local.select_value = 'A';
+ let child: InstanceType | undefined;
+ root.child(($$renderer) => {
+ child = $$renderer;
+ });
+
+ expect(child!.local.select_value).toBe('A');
+ child!.local.select_value = 'B';
+ expect(root.local.select_value).toBe('A');
+});
+
+test('subsume replaces tree content and state from other', () => {
+ const a = new Renderer(new SSRState('async'));
+ a.type = 'head';
+
+ a.push(' ');
+ a.local.select_value = 'A';
+
+ const b = new Renderer(new SSRState('async'));
+ b.child(async ($$renderer) => {
+ await Promise.resolve();
+ $$renderer.push('body');
+ });
+ b.global.css.add({ hash: 'h', code: 'c' });
+ b.global.set_title('Title', [1]);
+ b.local.select_value = 'B';
+ b.promise = Promise.resolve();
+
+ a.subsume(b);
+
+ expect(a.type).toBe('body');
+ expect(a.local.select_value).toBe('B');
+ expect(a.promise).toBe(b.promise);
+});
+
+test('subsume refuses to switch modes', () => {
+ const a = new Renderer(new SSRState('sync'));
+ a.type = 'head';
+
+ a.push(' ');
+ a.local.select_value = 'A';
+
+ const b = new Renderer(new SSRState('async'));
+ b.child(async ($$renderer) => {
+ await Promise.resolve();
+ $$renderer.push('body');
+ });
+ b.global.css.add({ hash: 'h', code: 'c' });
+ b.global.set_title('Title', [1]);
+ b.local.select_value = 'B';
+ b.promise = Promise.resolve();
+
+ expect(() => a.subsume(b)).toThrow(
+ "invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
+ );
+});
+
+test('SSRState uid generator uses prefix', () => {
+ const state = new SSRState('sync', 'id-');
+ expect(state.uid()).toBe('id-s1');
+});
+
+test('SSRState title ordering favors later lexicographic paths', () => {
+ const state = new SSRState('sync');
+
+ state.set_title('A', [1]);
+ expect(state.get_title()).toBe('A');
+
+ // equal path -> unchanged
+ state.set_title('B', [1]);
+ expect(state.get_title()).toBe('A');
+
+ // earlier -> unchanged
+ state.set_title('C', [0, 9]);
+ expect(state.get_title()).toBe('A');
+
+ // later -> update
+ state.set_title('D', [2]);
+ expect(state.get_title()).toBe('D');
+
+ // longer but same prefix -> update
+ state.set_title('E', [2, 0]);
+ expect(state.get_title()).toBe('E');
+
+ // shorter (earlier) than current with same prefix -> unchanged
+ state.set_title('F', [2]);
+ expect(state.get_title()).toBe('E');
+});
+
+test('selects an option with an explicit value', () => {
+ const component = (renderer: Renderer) => {
+ renderer.select({ value: 2 }, (renderer) => {
+ renderer.option({ value: 1 }, (renderer) => renderer.push('one'));
+ renderer.option({ value: 2 }, (renderer) => renderer.push('two'));
+ renderer.option({ value: 3 }, (renderer) => renderer.push('three'));
+ });
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe(
+ 'one two three '
+ );
+});
+
+test('selects an option with an implicit value', () => {
+ const component = (renderer: Renderer) => {
+ renderer.select({ value: 'two' }, (renderer) => {
+ renderer.option({}, (renderer) => renderer.push('one'));
+ renderer.option({}, (renderer) => renderer.push('two'));
+ renderer.option({}, (renderer) => renderer.push('three'));
+ });
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe(
+ 'one two three '
+ );
+});
+
+test('select merges scoped css hash with static class', () => {
+ const component = (renderer: Renderer) => {
+ renderer.select(
+ { class: 'foo', value: 'foo' },
+ (renderer) => {
+ renderer.option({ value: 'foo' }, (renderer) => renderer.push('foo'));
+ },
+ 'svelte-hash'
+ );
+ };
+
+ const { head, body } = Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe(
+ 'foo '
+ );
+});
+
+describe('async', () => {
+ beforeAll(() => {
+ enable_async_mode_flag();
+ });
+
+ afterAll(() => {
+ disable_async_mode_flag();
+ });
+
+ test('awaiting renderer gets async content', async () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('1');
+ renderer.child(async ($$renderer) => {
+ await Promise.resolve();
+ $$renderer.push('2');
+ });
+ renderer.push('3');
+ };
+
+ const result = await Renderer.render(component as unknown as Component);
+ expect(result.head).toBe('');
+ expect(result.body).toBe('123');
+ expect(() => result.html).toThrow('html_deprecated');
+ });
+
+ test('push accepts async functions in async context', async () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('a');
+ renderer.push(async () => {
+ await Promise.resolve();
+ return 'b';
+ });
+ renderer.push('c');
+ };
+
+ const { head, body } = await Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe('abc');
+ });
+
+ test('push handles async functions with different timing', async () => {
+ const component = (renderer: Renderer) => {
+ renderer.push(async () => {
+ await Promise.resolve();
+ return 'fast';
+ });
+ renderer.push(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ return 'slow';
+ });
+ renderer.push('sync');
+ };
+
+ const { head, body } = await Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe('fastslowsync');
+ });
+
+ test('push async functions work with head content type', async () => {
+ const component = (renderer: Renderer) => {
+ renderer.head(($$renderer) => {
+ $$renderer.push(async () => {
+ await Promise.resolve();
+ return 'Async Title ';
+ });
+ });
+ };
+
+ const { head, body } = await Renderer.render(component as unknown as Component);
+ expect(body).toBe('');
+ expect(head).toBe('Async Title ');
+ });
+
+ test('push async functions can be mixed with child renderers', async () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('start-');
+ renderer.push(async () => {
+ await Promise.resolve();
+ return 'async-';
+ });
+ renderer.child(($$renderer) => {
+ $$renderer.push('child-');
+ });
+ renderer.push('-end');
+ };
+
+ const { head, body } = await Renderer.render(component as unknown as Component);
+ expect(head).toBe('');
+ expect(body).toBe('start-async-child--end');
+ });
+
+ test('push async functions are not supported in sync context', () => {
+ const component = (renderer: Renderer) => {
+ renderer.push('a');
+ renderer.push(() => Promise.resolve('b'));
+ };
+
+ expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid');
+ expect(() => Renderer.render(component as unknown as Component).html).toThrow('await_invalid');
+ expect(() => Renderer.render(component as unknown as Component).head).toThrow('await_invalid');
+ });
+
+ test('on_destroy yields callbacks in the correct order', async () => {
+ const destroyed: string[] = [];
+ const component = (renderer: Renderer) => {
+ renderer.component((renderer) => {
+ renderer.on_destroy(() => destroyed.push('a'));
+ // children should not alter relative order
+ renderer.child(async (renderer) => {
+ await Promise.resolve();
+ renderer.on_destroy(() => destroyed.push('b'));
+ renderer.on_destroy(() => destroyed.push('b*'));
+ });
+ // but child components should
+ renderer.component((renderer) => {
+ renderer.on_destroy(() => destroyed.push('c'));
+ });
+ renderer.child((renderer) => {
+ renderer.on_destroy(() => destroyed.push('d'));
+ });
+ renderer.component((renderer) => {
+ renderer.on_destroy(() => destroyed.push('e'));
+ });
+ });
+ };
+
+ await Renderer.render(component as unknown as Component);
+ expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']);
+ });
+});
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index 6b0fc146c4..53cefabc69 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -1,17 +1,20 @@
-export interface Component {
+import type { Element } from './dev';
+import type { Renderer } from './renderer';
+
+export interface SSRContext {
/** parent */
- p: null | Component;
- /** context */
+ p: null | SSRContext;
+ /** component context */
c: null | Map;
- /** ondestroy */
- d: null | Array<() => void>;
- /**
- * dev mode only: the component function
- */
+ /** renderer */
+ r: null | Renderer;
+ /** dev mode only: the current component function */
function?: any;
+ /** dev mode only: the current element */
+ element?: Element;
}
-export interface RenderOutput {
+export interface SyncRenderOutput {
/** HTML that goes into the `` */
head: string;
/** @deprecated use `body` instead */
@@ -19,3 +22,5 @@ export interface RenderOutput {
/** HTML that goes somewhere into the `` */
body: string;
}
+
+export type RenderOutput = SyncRenderOutput & PromiseLike;
diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js
new file mode 100644
index 0000000000..d8d9cd6d43
--- /dev/null
+++ b/packages/svelte/src/internal/server/warnings.js
@@ -0,0 +1,17 @@
+/* This file is generated by scripts/process-messages/index.js. Do not edit! */
+
+import { DEV } from 'esm-env';
+
+var bold = 'font-weight: bold';
+var normal = 'font-weight: normal';
+
+/**
+ * Attempted to use asynchronous rendering without `experimental.async` enabled
+ */
+export function experimental_async_ssr() {
+ if (DEV) {
+ console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal);
+ } else {
+ console.warn(`https://svelte.dev/e/experimental_async_ssr`);
+ }
+}
\ No newline at end of file
diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js
index c8758c1d4d..4ad550e8d6 100644
--- a/packages/svelte/src/internal/shared/attributes.js
+++ b/packages/svelte/src/internal/shared/attributes.js
@@ -22,6 +22,10 @@ const replacements = {
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
+ // attribute hidden for values other than "until-found" behaves like a boolean attribute
+ if (name === 'hidden' && value !== 'until-found') {
+ is_boolean = true;
+ }
if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 66685cb00b..6bcc35016a 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -2,22 +2,6 @@
import { DEV } from 'esm-env';
-/**
- * Cannot await outside a `` with a `pending` snippet
- * @returns {never}
- */
-export function await_outside_boundary() {
- if (DEV) {
- const error = new Error(`await_outside_boundary\nCannot await outside a \`\` with a \`pending\` snippet\nhttps://svelte.dev/e/await_outside_boundary`);
-
- error.name = 'Svelte error';
-
- throw error;
- } else {
- throw new Error(`https://svelte.dev/e/await_outside_boundary`);
- }
-}
-
/**
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}
diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js
index 60bd8c9fc6..b7d3e673bc 100644
--- a/packages/svelte/src/legacy/legacy-server.js
+++ b/packages/svelte/src/legacy/legacy-server.js
@@ -1,11 +1,15 @@
/** @import { SvelteComponent } from '../index.js' */
import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js';
import { render } from '../internal/server/index.js';
+import { async_mode_flag } from '../internal/flags/index.js';
+import * as w from '../internal/server/warnings.js';
// By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime
export { createClassComponent };
+/** @typedef {{ head: string, html: string, css: { code: string, map: null }}} LegacyRenderResult */
+
/**
* Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor.
*
@@ -21,16 +25,58 @@ export { createClassComponent };
*/
export function asClassComponent(component) {
const component_constructor = as_class_component(component);
- /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => { html: any; css: { code: string; map: any; }; head: string; } } */
+ /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => LegacyRenderResult & PromiseLike } */
const _render = (props, { context } = {}) => {
// @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode
const result = render(component, { props, context });
- return {
- css: { code: '', map: null },
- head: result.head,
- html: result.body
- };
+
+ const munged = Object.defineProperties(
+ /** @type {LegacyRenderResult & PromiseLike} */ ({}),
+ {
+ css: {
+ value: { code: '', map: null }
+ },
+ head: {
+ get: () => result.head
+ },
+ html: {
+ get: () => result.body
+ },
+ then: {
+ /**
+ * this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
+ *
+ * @template TResult1
+ * @template [TResult2=never]
+ * @param { (value: LegacyRenderResult) => TResult1 } onfulfilled
+ * @param { (reason: unknown) => TResult2 } onrejected
+ */
+ value: (onfulfilled, onrejected) => {
+ if (!async_mode_flag) {
+ w.experimental_async_ssr();
+ const user_result = onfulfilled({
+ css: munged.css,
+ head: munged.head,
+ html: munged.html
+ });
+ return Promise.resolve(user_result);
+ }
+
+ return result.then((result) => {
+ return onfulfilled({
+ css: munged.css,
+ head: result.head,
+ html: result.body
+ });
+ }, onrejected);
+ }
+ }
+ }
+ );
+
+ return munged;
};
+
// @ts-expect-error this is present for SSR
component_constructor.render = _render;
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index f8c39253ac..f8a7e8d46d 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -154,7 +154,6 @@ const DOM_BOOLEAN_ATTRIBUTES = [
'default',
'disabled',
'formnovalidate',
- 'hidden',
'indeterminate',
'inert',
'ismap',
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 67c586790f..6d69b57447 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.38.6';
+export const VERSION = '5.39.8';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/dynamic-element-binding-invalid/_config.js b/packages/svelte/tests/compiler-errors/samples/dynamic-element-binding-invalid/_config.js
index 4bb3edc27b..25b0827765 100644
--- a/packages/svelte/tests/compiler-errors/samples/dynamic-element-binding-invalid/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/dynamic-element-binding-invalid/_config.js
@@ -3,6 +3,6 @@ import { test } from '../../test';
export default test({
error: {
code: 'bind_invalid_target',
- message: '`bind:value` can only be used with ,
+ 1/2
`
);
- assert.deepEqual(logs, [0, 0]);
+ assert.deepEqual(logs, [0, 0, 0, 0]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte
index bbad2cdf4a..c0dd86993a 100644
--- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte
@@ -17,10 +17,14 @@
$effect(() => {
foo = new Foo();
});
+
+ let bar = $derived(new Foo());
- foo.increment()}>increment
+ {foo.increment(); bar.increment()}}>increment
{#if foo}
{foo.value}/{foo.double}
{/if}
+
+{bar.value}/{bar.double}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-children/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-children/_expected.html
new file mode 100644
index 0000000000..04d9709792
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-children/_expected.html
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-children/component.svelte b/packages/svelte/tests/server-side-rendering/samples/async-children/component.svelte
new file mode 100644
index 0000000000..73b5a2f823
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-children/component.svelte
@@ -0,0 +1,4 @@
+
+{@render children()}
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-children/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-children/main.svelte
new file mode 100644
index 0000000000..ae0fc571b3
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-children/main.svelte
@@ -0,0 +1,7 @@
+
+
+ {@const one = await 1}
+ {one}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js
new file mode 100644
index 0000000000..a1b52a2df9
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js
@@ -0,0 +1,6 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['async'],
+ error: 'lifecycle_outside_component'
+});
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte
new file mode 100644
index 0000000000..8e770c214b
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte
@@ -0,0 +1,7 @@
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/_expected.html
new file mode 100644
index 0000000000..d46a957bdf
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/_expected.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/main.svelte
new file mode 100644
index 0000000000..ed4270b754
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-directive-with-spreading/main.svelte
@@ -0,0 +1,2 @@
+
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html
new file mode 100644
index 0000000000..bf0d87ab1b
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html
@@ -0,0 +1 @@
+4
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte
new file mode 100644
index 0000000000..e580345a2e
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte
@@ -0,0 +1,5 @@
+{#each await Promise.resolve([]) as item}
+ {await Promise.reject('This should never be reached')}
+{:else}
+ {await Promise.resolve(4)}
+{/each}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html
new file mode 100644
index 0000000000..d800886d9c
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html
@@ -0,0 +1 @@
+123
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte
new file mode 100644
index 0000000000..4b6bf7eadc
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte
@@ -0,0 +1,9 @@
+
+
+{#each await Promise.resolve([first, second, third]) as item}
+ {await item}
+{/each}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte
new file mode 100644
index 0000000000..5ddccbe17c
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte
@@ -0,0 +1,9 @@
+
+
+
+ {#if await promise}
+ A
+ {/if}
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte
new file mode 100644
index 0000000000..95b102e680
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte
@@ -0,0 +1,7 @@
+
+
+
+ {(await promise) && 'B'}
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js
new file mode 100644
index 0000000000..f47bee71df
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html
new file mode 100644
index 0000000000..af5c5feba4
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html
@@ -0,0 +1 @@
+B
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte
new file mode 100644
index 0000000000..9b49cf1c22
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte
@@ -0,0 +1,27 @@
+
+
+
+ {#if await main.promise}
+ Main
+ {/if}
+
+
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-html-tag/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-html-tag/_expected.html
new file mode 100644
index 0000000000..5be0be37f2
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-html-tag/_expected.html
@@ -0,0 +1 @@
+ this should work
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-html-tag/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-html-tag/main.svelte
new file mode 100644
index 0000000000..2d556e4d5b
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-html-tag/main.svelte
@@ -0,0 +1 @@
+{@html await 'this should work'}
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html
new file mode 100644
index 0000000000..c3b0e19566
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html
@@ -0,0 +1 @@
+yes yes yes
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte
new file mode 100644
index 0000000000..a97f32f034
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte
@@ -0,0 +1,5 @@
+{#if await Promise.resolve(false)}
+ {await Promise.reject('no no no')}
+{:else}
+ {await Promise.resolve('yes yes yes')}
+{/if}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html
new file mode 100644
index 0000000000..c3b0e19566
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html
@@ -0,0 +1 @@
+yes yes yes
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte
new file mode 100644
index 0000000000..4ca3462771
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte
@@ -0,0 +1,5 @@
+{#if await Promise.resolve(true)}
+ {await Promise.resolve('yes yes yes')}
+{:else}
+ {await Promise.reject('no no no')}
+{/if}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/_expected.html
new file mode 100644
index 0000000000..1cc0b4b00f
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/_expected.html
@@ -0,0 +1 @@
+awaited
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/main.svelte
new file mode 100644
index 0000000000..b4e8dbcb75
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-nullish-pending-snippet/main.svelte
@@ -0,0 +1,6 @@
+
+
+ {await 'awaited'}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html
new file mode 100644
index 0000000000..f2fd8e71b9
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html
@@ -0,0 +1,3 @@
+
+ 421
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte
new file mode 100644
index 0000000000..2ad9b64509
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte
@@ -0,0 +1,3 @@
+
+ {await Promise.resolve(42)}1
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html
new file mode 100644
index 0000000000..f642109218
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html
@@ -0,0 +1,3 @@
+
+ 42
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte
new file mode 100644
index 0000000000..a5d91424cb
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte
@@ -0,0 +1,3 @@
+
+ {await Promise.resolve(42)}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte
new file mode 100644
index 0000000000..a17413bd8d
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte
@@ -0,0 +1,5 @@
+
+
+{@render props.children?.()}
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html
new file mode 100644
index 0000000000..96d1d8b233
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- Dog Cat
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte
new file mode 100644
index 0000000000..aaf10c2cb3
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte
@@ -0,0 +1,8 @@
+
+
+ --Please choose an option--
+ Dog
+ Cat
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html
new file mode 100644
index 0000000000..de7cba8837
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- dog cat
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte
new file mode 100644
index 0000000000..2cc4065eb6
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte
@@ -0,0 +1,13 @@
+
+
+ {@render option("--Please choose an option--")}
+
+
+ {@render option(Promise.resolve('dog'))}
+
+
+ {@render option(Promise.resolve('cat'))}
+
+
+
+{#snippet option(val)}{await val}{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html
new file mode 100644
index 0000000000..de7cba8837
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- dog cat
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte
new file mode 100644
index 0000000000..8d9393cf6c
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte
@@ -0,0 +1,5 @@
+
+ --Please choose an option--
+ {await Promise.resolve('dog')}
+ cat
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-snippet/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-snippet/_expected.html
new file mode 100644
index 0000000000..5be0be37f2
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-snippet/_expected.html
@@ -0,0 +1 @@
+this should work
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-snippet/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-snippet/main.svelte
new file mode 100644
index 0000000000..a6f2ac7b09
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-snippet/main.svelte
@@ -0,0 +1,6 @@
+{#snippet foo()}
+ {@const x = await 'this should work'}
+ {x}
+{/snippet}
+
+{@render foo()}
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/_expected.html
new file mode 100644
index 0000000000..5be0be37f2
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/_expected.html
@@ -0,0 +1 @@
+this should work
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/main.svelte
new file mode 100644
index 0000000000..c4cde60119
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/async-svelte-boundary/main.svelte
@@ -0,0 +1,4 @@
+
+ {@const x = await 'this should work'}
+ {x}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html
new file mode 100644
index 0000000000..80937efaee
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html
@@ -0,0 +1 @@
+A
B
C
diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte
new file mode 100644
index 0000000000..6738b34054
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte
@@ -0,0 +1,3 @@
+A
+B
+C
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte b/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte
new file mode 100644
index 0000000000..fab18ea195
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/context/Child.svelte
@@ -0,0 +1,8 @@
+
+
+{getContext('key')}
diff --git a/packages/svelte/tests/server-side-rendering/samples/context/_config.js b/packages/svelte/tests/server-side-rendering/samples/context/_config.js
new file mode 100644
index 0000000000..05de37a8bd
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/context/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['async']
+});
diff --git a/packages/svelte/tests/server-side-rendering/samples/context/_expected.html b/packages/svelte/tests/server-side-rendering/samples/context/_expected.html
new file mode 100644
index 0000000000..554133d8d9
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/context/_expected.html
@@ -0,0 +1,3 @@
+value
+value
+child value
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/context/main.svelte b/packages/svelte/tests/server-side-rendering/samples/context/main.svelte
new file mode 100644
index 0000000000..fd75a6cbae
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/context/main.svelte
@@ -0,0 +1,11 @@
+
+
+{getContext('key')}
+{(await Promise.resolve(true)) && getContext('key')}
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected.html
index 60e974bd1a..7ba4de394e 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected.html
@@ -1 +1 @@
-foo
\ No newline at end of file
+foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected_head.html
index 5350e77a49..99db8fc6dc 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected_head.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-minify/_expected_head.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css
index bddefdd00c..81c1111967 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css
@@ -1,4 +1,4 @@
-.foo.svelte-sg04hs {
+.foo.svelte-1nvcr6w {
color: red;
}
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html
index 1f0b2b95fe..a6eba00942 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html
@@ -1 +1 @@
-bar
foo
\ No newline at end of file
+bar
foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html
index 6d795670ff..516a9576b3 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css
index 8882c6ec7e..eb1f3e7a9b 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css
@@ -1,4 +1,4 @@
- .foo.svelte-sg04hs {
+ .foo.svelte-okauro {
color: red;
}
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html
index 8ebe1ad73e..01ebd79914 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html
@@ -1 +1 @@
-foo
\ No newline at end of file
+foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html
index 9c4f8a8538..1a1511f55e 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css
index 8882c6ec7e..ec86890478 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css/_expected.css
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css
@@ -1,4 +1,4 @@
- .foo.svelte-sg04hs {
+ .foo.svelte-e9omc {
color: red;
}
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html
index 8ebe1ad73e..dc9409fa03 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css/_expected.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html
@@ -1 +1 @@
-foo
\ No newline at end of file
+foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html
index 9c4f8a8538..941c1f13b4 100644
--- a/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js b/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js
index 45054bbada..f47bee71df 100644
--- a/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js
+++ b/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_config.js
@@ -1,5 +1,3 @@
import { test } from '../../test';
-export default test({
- props: { adjective: 'custom' }
-});
+export default test({});
diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
index 71edff6a68..6325ea7d0e 100644
--- a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
+++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js
@@ -4,8 +4,6 @@ export default test({
compileOptions: {
dev: true
},
-
- errors: [
+ error:
'node_invalid_placement_ssr: `` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:2:1) cannot be a child of `
` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
- ]
});
diff --git a/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/_expected.html b/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/_expected.html
new file mode 100644
index 0000000000..b30e5ff4a1
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/_expected.html
@@ -0,0 +1,2 @@
+foo
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/main.svelte b/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/main.svelte
new file mode 100644
index 0000000000..657f1e949d
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/option-scoped-class/main.svelte
@@ -0,0 +1,10 @@
+
+ foo
+
+
+
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/_expected.html
new file mode 100644
index 0000000000..d74fc96d9c
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/_expected.html
@@ -0,0 +1 @@
+foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/main.svelte
new file mode 100644
index 0000000000..9ad3a6b4d2
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-scoped-class/main.svelte
@@ -0,0 +1,9 @@
+
+ foo
+
+
+
diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts
index 3e57539427..7eede332a7 100644
--- a/packages/svelte/tests/server-side-rendering/test.ts
+++ b/packages/svelte/tests/server-side-rendering/test.ts
@@ -9,79 +9,133 @@ import { assert } from 'vitest';
import { render } from 'svelte/server';
import { compile_directory, should_update_expected, try_read_file } from '../helpers.js';
import { assert_html_equal_with_options } from '../html_equal.js';
-import { suite, type BaseTest } from '../suite.js';
+import { suite_with_variants, type BaseTest } from '../suite.js';
import type { CompileOptions } from '#compiler';
+import { seen } from '../../src/internal/server/dev.js';
interface SSRTest extends BaseTest {
+ mode?: ('sync' | 'async')[];
compileOptions?: Partial;
+ load_compiled?: boolean;
props?: Record;
id_prefix?: string;
withoutNormalizeHtml?: boolean;
- errors?: string[];
+ error?: string;
}
-// eslint-disable-next-line no-console
-let console_error = console.error;
+// TODO remove this shim when we can
+// @ts-expect-error
+Promise.withResolvers = () => {
+ let resolve;
+ let reject;
+
+ const promise = new Promise((f, r) => {
+ resolve = f;
+ reject = r;
+ });
+
+ return { promise, resolve, reject };
+};
+
+const { test, run } = suite_with_variants(
+ ['sync', 'async'],
+ (variant, config, test_name) => {
+ if (config.mode && !config.mode.includes(variant)) {
+ return 'no-test';
+ }
-const { test, run } = suite(async (config, test_dir) => {
- await compile_directory(test_dir, 'server', config.compileOptions);
+ if (test_name.startsWith('async') && variant === 'sync') {
+ return 'no-test';
+ }
- const errors: string[] = [];
+ return false;
+ },
+ async (config, test_dir) => {
+ const compile_options = {
+ experimental: {
+ async: true,
+ ...config.compileOptions?.experimental
+ },
+ ...config.compileOptions
+ };
+
+ if (!config.load_compiled) {
+ await compile_directory(test_dir, 'server', compile_options);
+ }
- console.error = (...args) => {
- errors.push(...args);
- };
+ return compile_options;
+ },
+ async (config, test_dir, variant, compile_options) => {
+ const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
+ const expected_html = try_read_file(`${test_dir}/_expected.html`);
+ const is_async = variant === 'async';
- const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
- const expected_html = try_read_file(`${test_dir}/_expected.html`);
- const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix });
- const { body, head } = rendered;
+ seen?.clear();
+
+ let rendered;
+ try {
+ const render_result = render(Component, {
+ props: config.props || {},
+ idPrefix: config.id_prefix
+ });
+ rendered = is_async ? await render_result : render_result;
+ } catch (error) {
+ if (config.error) {
+ assert.deepEqual((error as Error).message, config.error);
+ return;
+ } else {
+ throw error;
+ }
+ }
- fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);
+ const { body, head } = rendered;
- if (head) {
- fs.writeFileSync(`${test_dir}/_output/rendered_head.html`, head);
- }
+ fs.writeFileSync(
+ `${test_dir}/_output/${is_async ? 'async_rendered.html' : 'rendered.html'}`,
+ body
+ );
- try {
- assert_html_equal_with_options(body, expected_html || '', {
- preserveComments: config.compileOptions?.preserveComments,
- withoutNormalizeHtml: config.withoutNormalizeHtml
- });
- } catch (error: any) {
- if (should_update_expected()) {
- fs.writeFileSync(`${test_dir}/_expected.html`, body);
- console.log(`Updated ${test_dir}/_expected.html.`);
- } else {
- error.message += '\n' + `${test_dir}/main.svelte`;
- throw error;
+ if (head) {
+ fs.writeFileSync(
+ `${test_dir}/_output/${is_async ? 'async_rendered_head.html' : 'rendered_head.html'}`,
+ head
+ );
}
- }
- if (fs.existsSync(`${test_dir}/_expected_head.html`)) {
try {
- assert_html_equal_with_options(
- head,
- fs.readFileSync(`${test_dir}/_expected_head.html`, 'utf-8'),
- {}
- );
+ assert_html_equal_with_options(body, expected_html || '', {
+ preserveComments: compile_options.preserveComments,
+ withoutNormalizeHtml: config.withoutNormalizeHtml
+ });
} catch (error: any) {
if (should_update_expected()) {
- fs.writeFileSync(`${test_dir}/_expected_head.html`, head);
- console.log(`Updated ${test_dir}/_expected_head.html.`);
- error.message += '\n' + `${test_dir}/main.svelte`;
+ fs.writeFileSync(`${test_dir}/_expected.html`, body);
+ console.log(`Updated ${test_dir}/_expected.html.`);
} else {
+ error.message += '\n' + `${test_dir}/main.svelte`;
throw error;
}
}
- }
- if (errors.length > 0) {
- assert.deepEqual(config.errors, errors);
+ if (fs.existsSync(`${test_dir}/_expected_head.html`)) {
+ try {
+ assert_html_equal_with_options(
+ head,
+ fs.readFileSync(`${test_dir}/_expected_head.html`, 'utf-8'),
+ {}
+ );
+ } catch (error: any) {
+ if (should_update_expected()) {
+ fs.writeFileSync(`${test_dir}/_expected_head.html`, head);
+ console.log(`Updated ${test_dir}/_expected_head.html.`);
+ error.message += '\n' + `${test_dir}/main.svelte`;
+ } else {
+ throw error;
+ }
+ }
+ }
}
-
- console.error = console_error;
-});
+);
export { test };
diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js
new file mode 100644
index 0000000000..2e30bbeb16
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({ compileOptions: { experimental: { async: true } } });
diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..cf667e1624
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js
@@ -0,0 +1,35 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/client';
+
+export default function Async_each_fallback_hoisting($$anchor) {
+ var fragment = $.comment();
+ var node = $.first_child(fragment);
+
+ $.async(node, [() => Promise.resolve([])], (node, $$collection) => {
+ $.each(
+ node,
+ 16,
+ () => $.get($$collection),
+ $.index,
+ ($$anchor, item) => {
+ $.next();
+
+ var text = $.text();
+
+ $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('This should never be reached')]);
+ $.append($$anchor, text);
+ },
+ ($$anchor) => {
+ $.next();
+
+ var text_1 = $.text();
+
+ $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve(4)]);
+ $.append($$anchor, text_1);
+ }
+ );
+ });
+
+ $.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..c579fda929
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js
@@ -0,0 +1,25 @@
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/server';
+
+export default function Async_each_fallback_hoisting($$renderer) {
+ $$renderer.async(async ($$renderer) => {
+ const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))());
+
+ if (each_array.length !== 0) {
+ $$renderer.push('');
+
+ for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
+ let item = each_array[$$index];
+
+ $$renderer.push(``);
+ $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached')));
+ }
+ } else {
+ $$renderer.push('');
+ $$renderer.push(``);
+ $$renderer.push(async () => $.escape(await Promise.resolve(4)));
+ }
+ });
+
+ $$renderer.push(``);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte
new file mode 100644
index 0000000000..e580345a2e
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte
@@ -0,0 +1,5 @@
+{#each await Promise.resolve([]) as item}
+ {await Promise.reject('This should never be reached')}
+{:else}
+ {await Promise.resolve(4)}
+{/each}
diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js
new file mode 100644
index 0000000000..2e30bbeb16
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({ compileOptions: { experimental: { async: true } } });
diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..a1535d6886
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js
@@ -0,0 +1,24 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/client';
+
+export default function Async_each_hoisting($$anchor) {
+ const first = Promise.resolve(1);
+ const second = Promise.resolve(2);
+ const third = Promise.resolve(3);
+ var fragment = $.comment();
+ var node = $.first_child(fragment);
+
+ $.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => {
+ $.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => {
+ $.next();
+
+ var text = $.text();
+
+ $.template_effect(($0) => $.set_text(text, $0), void 0, [() => $.get(item)]);
+ $.append($$anchor, text);
+ });
+ });
+
+ $.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..e87b50e2a4
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js
@@ -0,0 +1,23 @@
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/server';
+
+export default function Async_each_hoisting($$renderer) {
+ const first = Promise.resolve(1);
+ const second = Promise.resolve(2);
+ const third = Promise.resolve(3);
+
+ $$renderer.push(``);
+
+ $$renderer.async(async ($$renderer) => {
+ const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))());
+
+ for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
+ let item = each_array[$$index];
+
+ $$renderer.push(``);
+ $$renderer.push(async () => $.escape(await item));
+ }
+ });
+
+ $$renderer.push(``);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte
new file mode 100644
index 0000000000..4b6bf7eadc
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte
@@ -0,0 +1,9 @@
+
+
+{#each await Promise.resolve([first, second, third]) as item}
+ {await item}
+{/each}
diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js
new file mode 100644
index 0000000000..2e30bbeb16
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({ compileOptions: { experimental: { async: true } } });
diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..e385f5d234
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js
@@ -0,0 +1,30 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/client';
+
+export default function Async_if_alternate_hoisting($$anchor) {
+ var fragment = $.comment();
+ var node = $.first_child(fragment);
+
+ $.async(node, [() => Promise.resolve(false)], (node, $$condition) => {
+ var consequent = ($$anchor) => {
+ var text = $.text();
+
+ $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.reject('no no no')]);
+ $.append($$anchor, text);
+ };
+
+ var alternate = ($$anchor) => {
+ var text_1 = $.text();
+
+ $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.resolve('yes yes yes')]);
+ $.append($$anchor, text_1);
+ };
+
+ $.if(node, ($$render) => {
+ if ($.get($$condition)) $$render(consequent); else $$render(alternate, false);
+ });
+ });
+
+ $.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..df4ad80899
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js
@@ -0,0 +1,16 @@
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/server';
+
+export default function Async_if_alternate_hoisting($$renderer) {
+ $$renderer.async(async ($$renderer) => {
+ if ((await $.save(Promise.resolve(false)))()) {
+ $$renderer.push('');
+ $$renderer.push(async () => $.escape(await Promise.reject('no no no')));
+ } else {
+ $$renderer.push('');
+ $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes')));
+ }
+ });
+
+ $$renderer.push(``);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte
new file mode 100644
index 0000000000..a97f32f034
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte
@@ -0,0 +1,5 @@
+{#if await Promise.resolve(false)}
+ {await Promise.reject('no no no')}
+{:else}
+ {await Promise.resolve('yes yes yes')}
+{/if}
diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js
new file mode 100644
index 0000000000..2e30bbeb16
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({ compileOptions: { experimental: { async: true } } });
diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..356e8e9607
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js
@@ -0,0 +1,30 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/client';
+
+export default function Async_if_hoisting($$anchor) {
+ var fragment = $.comment();
+ var node = $.first_child(fragment);
+
+ $.async(node, [() => Promise.resolve(true)], (node, $$condition) => {
+ var consequent = ($$anchor) => {
+ var text = $.text();
+
+ $.template_effect(($0) => $.set_text(text, $0), void 0, [() => Promise.resolve('yes yes yes')]);
+ $.append($$anchor, text);
+ };
+
+ var alternate = ($$anchor) => {
+ var text_1 = $.text();
+
+ $.template_effect(($0) => $.set_text(text_1, $0), void 0, [() => Promise.reject('no no no')]);
+ $.append($$anchor, text_1);
+ };
+
+ $.if(node, ($$render) => {
+ if ($.get($$condition)) $$render(consequent); else $$render(alternate, false);
+ });
+ });
+
+ $.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..1d935f9be8
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js
@@ -0,0 +1,16 @@
+import 'svelte/internal/flags/async';
+import * as $ from 'svelte/internal/server';
+
+export default function Async_if_hoisting($$renderer) {
+ $$renderer.async(async ($$renderer) => {
+ if ((await $.save(Promise.resolve(true)))()) {
+ $$renderer.push('');
+ $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes')));
+ } else {
+ $$renderer.push('');
+ $$renderer.push(async () => $.escape(await Promise.reject('no no no')));
+ }
+ });
+
+ $$renderer.push(``);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte b/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte
new file mode 100644
index 0000000000..4ca3462771
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte
@@ -0,0 +1,5 @@
+{#if await Promise.resolve(true)}
+ {await Promise.resolve('yes yes yes')}
+{:else}
+ {await Promise.reject('no no no')}
+{/if}
diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
index cc2628c852..e9bf215dcd 100644
--- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
@@ -1,6 +1,6 @@
import * as $ from 'svelte/internal/server';
-export default function Await_block_scope($$payload) {
+export default function Await_block_scope($$renderer) {
let counter = { count: 0 };
const promise = Promise.resolve(counter);
@@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) {
counter.count += 1;
}
- $$payload.out.push(`clicks: ${$.escape(counter.count)} `);
- $.await($$payload, promise, () => {}, (counter) => {});
- $$payload.out.push(` ${$.escape(counter.count)}`);
+ $$renderer.push(`clicks: ${$.escape(counter.count)} `);
+ $.await($$renderer, promise, () => {}, (counter) => {});
+ $$renderer.push(` ${$.escape(counter.count)}`);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
index c0db7d2fd5..2ef3a429ba 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
@@ -1,18 +1,18 @@
import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte';
-function snippet($$payload) {
- $$payload.out.push(`Something`);
+function snippet($$renderer) {
+ $$renderer.push(`Something`);
}
-export default function Bind_component_snippet($$payload) {
+export default function Bind_component_snippet($$renderer) {
let value = '';
const _snippet = snippet;
let $$settled = true;
- let $$inner_payload;
+ let $$inner_renderer;
- function $$render_inner($$payload) {
- TextInput($$payload, {
+ function $$render_inner($$renderer) {
+ TextInput($$renderer, {
get value() {
return value;
},
@@ -23,14 +23,14 @@ export default function Bind_component_snippet($$payload) {
}
});
- $$payload.out.push(` value: ${$.escape(value)}`);
+ $$renderer.push(` value: ${$.escape(value)}`);
}
do {
$$settled = true;
- $$inner_payload = $.copy_payload($$payload);
- $$render_inner($$inner_payload);
+ $$inner_renderer = $$renderer.copy();
+ $$render_inner($$inner_renderer);
} while (!$$settled);
- $.assign_payload($$payload, $$inner_payload);
+ $$renderer.subsume($$inner_renderer);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js
index 148573766f..8697d71bfa 100644
--- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js
@@ -1,5 +1,5 @@
import * as $ from 'svelte/internal/server';
-export default function Bind_this($$payload) {
- Foo($$payload, {});
+export default function Bind_this($$renderer) {
+ Foo($$renderer, {});
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
index abfc264fea..57b81d86ba 100644
--- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
@@ -1,38 +1,36 @@
import * as $ from 'svelte/internal/server';
-export default function Class_state_field_constructor_assignment($$payload, $$props) {
- $.push();
-
- class Foo {
- a = 0;
- #b;
- #foo = $.derived(() => ({ bar: this.a * 2 }));
-
- get foo() {
- return this.#foo();
- }
-
- set foo($$value) {
- return this.#foo($$value);
- }
-
- #bar = $.derived(() => ({ baz: this.foo }));
-
- get bar() {
- return this.#bar();
- }
-
- set bar($$value) {
- return this.#bar($$value);
+export default function Class_state_field_constructor_assignment($$renderer, $$props) {
+ $$renderer.component(($$renderer) => {
+ class Foo {
+ a = 0;
+ #b;
+ #foo = $.derived(() => ({ bar: this.a * 2 }));
+
+ get foo() {
+ return this.#foo();
+ }
+
+ set foo($$value) {
+ return this.#foo($$value);
+ }
+
+ #bar = $.derived(() => ({ baz: this.foo }));
+
+ get bar() {
+ return this.#bar();
+ }
+
+ set bar($$value) {
+ return this.#bar($$value);
+ }
+
+ constructor() {
+ this.a = 1;
+ this.#b = 2;
+ this.foo.bar = 3;
+ this.bar = 4;
+ }
}
-
- constructor() {
- this.a = 1;
- this.#b = 2;
- this.foo.bar = 3;
- this.bar = 4;
- }
- }
-
- $.pop();
+ });
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js
index ac3dfcd2cb..51241b92c0 100644
--- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js
@@ -1,13 +1,13 @@
import * as $ from 'svelte/internal/server';
-export default function Delegated_locally_declared_shadowed($$payload) {
- const each_array = $.ensure_array_like({ length: 1 });
+export default function Delegated_locally_declared_shadowed($$renderer) {
+ $$renderer.push(``);
- $$payload.out.push(``);
+ const each_array = $.ensure_array_like({ length: 1 });
for (let index = 0, $$length = each_array.length; index < $$length; index++) {
- $$payload.out.push(`B `);
+ $$renderer.push(`B `);
}
- $$payload.out.push(``);
+ $$renderer.push(``);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
index 9c837d4e1d..1ff8402974 100644
--- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
@@ -1,10 +1,10 @@
import * as $ from 'svelte/internal/server';
-export default function Main($$payload) {
+export default function Main($$renderer) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
- $$payload.out.push(`
`);
+ $$renderer.push(`
`);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js
index 8fa0c5f28c..e6de07a36e 100644
--- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js
@@ -1,13 +1,13 @@
import * as $ from 'svelte/internal/server';
-export default function Each_index_non_null($$payload) {
- const each_array = $.ensure_array_like(Array(10));
+export default function Each_index_non_null($$renderer) {
+ $$renderer.push(``);
- $$payload.out.push(``);
+ const each_array = $.ensure_array_like(Array(10));
for (let i = 0, $$length = each_array.length; i < $$length; i++) {
- $$payload.out.push(`index: ${$.escape(i)}
`);
+ $$renderer.push(`index: ${$.escape(i)}
`);
}
- $$payload.out.push(``);
+ $$renderer.push(``);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
index 6dbe8130da..e1c550e113 100644
--- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
@@ -1,15 +1,15 @@
import * as $ from 'svelte/internal/server';
-export default function Each_string_template($$payload) {
- const each_array = $.ensure_array_like(['foo', 'bar', 'baz']);
+export default function Each_string_template($$renderer) {
+ $$renderer.push(``);
- $$payload.out.push(``);
+ const each_array = $.ensure_array_like(['foo', 'bar', 'baz']);
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let thing = each_array[$$index];
- $$payload.out.push(`${$.escape(thing)}, `);
+ $$renderer.push(`${$.escape(thing)}, `);
}
- $$payload.out.push(``);
+ $$renderer.push(``);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
index ce4f09ed1d..855ae30d21 100644
--- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
@@ -1,6 +1,6 @@
import * as $ from 'svelte/internal/server';
-export default function Function_prop_no_getter($$payload) {
+export default function Function_prop_no_getter($$renderer) {
let count = 0;
function onmouseup() {
@@ -9,13 +9,13 @@ export default function Function_prop_no_getter($$payload) {
const plusOne = (num) => num + 1;
- Button($$payload, {
+ Button($$renderer, {
onmousedown: () => count += 1,
onmouseup,
onmouseenter: () => count = plusOne(count),
- children: ($$payload) => {
- $$payload.out.push(`clicks: ${$.escape(count)}`);
+ children: ($$renderer) => {
+ $$renderer.push(`clicks: ${$.escape(count)}`);
},
$$slots: { default: true }
diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js
index b1a0a5f9e6..a5eb404d9e 100644
--- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js
@@ -1,5 +1,5 @@
import * as $ from 'svelte/internal/server';
-export default function Functional_templating($$payload) {
- $$payload.out.push(`hello child element
another child element
`);
+export default function Functional_templating($$renderer) {
+ $$renderer.push(`hello child element
another child element
`);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js
index 30f6d6b74a..5c1a710f2e 100644
--- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js
@@ -1,5 +1,5 @@
import * as $ from 'svelte/internal/server';
-export default function Hello_world($$payload) {
- $$payload.out.push(`hello world `);
+export default function Hello_world($$renderer) {
+ $$renderer.push(`hello world `);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js
index ea1d12c83b..d8b9d98b03 100644
--- a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js
@@ -1,5 +1,5 @@
import * as $ from 'svelte/internal/server';
-export default function Hmr($$payload) {
- $$payload.out.push(`hello world `);
+export default function Hmr($$renderer) {
+ $$renderer.push(`hello world `);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
index 2ed863d68f..2d7b9906ed 100644
--- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
@@ -1,4 +1,4 @@
import * as $ from 'svelte/internal/server';
import { random } from './module.svelte';
-export default function Imports_in_modules($$payload) {}
\ No newline at end of file
+export default function Imports_in_modules($$renderer) {}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
index 18e01b4f72..a7e580acb8 100644
--- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
@@ -1,8 +1,8 @@
import * as $ from 'svelte/internal/server';
-export default function Nullish_coallescence_omittance($$payload) {
+export default function Nullish_coallescence_omittance($$renderer) {
let name = 'world';
let count = 0;
- $$payload.out.push(`Hello, world! 123 Count is ${$.escape(count)} Hello, world `);
+ $$renderer.push(`Hello, world! 123 Count is ${$.escape(count)} Hello, world `);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js
index 33a3633939..6db24ac621 100644
--- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js
@@ -1,16 +1,15 @@
import * as $ from 'svelte/internal/server';
-export default function Props_identifier($$payload, $$props) {
- $.push();
+export default function Props_identifier($$renderer, $$props) {
+ $$renderer.component(($$renderer) => {
+ let { $$slots, $$events, ...props } = $$props;
- let { $$slots, $$events, ...props } = $$props;
-
- props.a;
- props[a];
- props.a.b;
- props.a.b = true;
- props.a = true;
- props[a] = true;
- props;
- $.pop();
+ props.a;
+ props[a];
+ props.a.b;
+ props.a.b = true;
+ props.a = true;
+ props[a] = true;
+ props;
+ });
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
index 29b0d0d594..9f76d8ffb8 100644
--- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
@@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server';
-export default function Purity($$payload) {
- $$payload.out.push(`0
${$.escape(location.href)}
`);
- Child($$payload, { prop: encodeURIComponent('hello') });
- $$payload.out.push(``);
+export default function Purity($$renderer) {
+ $$renderer.push(`0
${$.escape(location.href)}
`);
+ Child($$renderer, { prop: encodeURIComponent('hello') });
+ $$renderer.push(``);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js
index bad475ec86..7a9f6193d7 100644
--- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js
@@ -1,7 +1,13 @@
import * as $ from 'svelte/internal/server';
-export default function Skip_static_subtree($$payload, $$props) {
+export default function Skip_static_subtree($$renderer, $$props) {
let { title, content } = $$props;
- $$payload.out.push(` ${$.escape(title)} we don't need to traverse these nodes
or
these
ones
${$.html(content)} these
trailing
nodes
can
be
completely
ignored
a `);
+ $$renderer.push(` ${$.escape(title)} we don't need to traverse these nodes
or
these
ones
${$.html(content)} these
trailing
nodes
can
be
completely
ignored
`);
+
+ $$renderer.option({ value: 'a' }, ($$renderer) => {
+ $$renderer.push(`a`);
+ });
+
+ $$renderer.push(` `);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js
index c2736b0f43..4ab7f90c58 100644
--- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js
@@ -1,6 +1,6 @@
import * as $ from 'svelte/internal/server';
-export default function State_proxy_literal($$payload) {
+export default function State_proxy_literal($$renderer) {
let str = '';
let tpl = ``;
@@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) {
tpl = ``;
}
- $$payload.out.push(` reset `);
+ $$renderer.push(` reset `);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js
index 4426ad1164..fc97686bb1 100644
--- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js
@@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server';
-export default function Svelte_element($$payload, $$props) {
+export default function Svelte_element($$renderer, $$props) {
let { tag = 'hr' } = $$props;
- $.element($$payload, tag);
+ $.element($$renderer, tag);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js
index f7dc586026..f886f9fbe3 100644
--- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js
@@ -1,6 +1,6 @@
import * as $ from 'svelte/internal/server';
-export default function Text_nodes_deriveds($$payload) {
+export default function Text_nodes_deriveds($$renderer) {
let count1 = 0;
let count2 = 0;
@@ -12,5 +12,5 @@ export default function Text_nodes_deriveds($$payload) {
return count2;
}
- $$payload.out.push(`${$.escape(text1())}${$.escape(text2())}
`);
+ $$renderer.push(`${$.escape(text1())}${$.escape(text2())}
`);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js
index ee9a3d92c4..3a292ff428 100644
--- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js
+++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js
@@ -57,7 +57,7 @@ export default test({
{ str: 'replace_me_script', strGenerated: 'done_replace_script_2' },
{ str: 'done_replace_script_2', idxGenerated: 1 }
],
- css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-o6vre' }],
+ css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-1vsrjd4' }],
test({ assert, code_preprocessed, code_css }) {
assert.equal(
code_preprocessed.includes('\n/*# sourceMappingURL=data:application/json;base64,'),
diff --git a/packages/svelte/tests/sourcemaps/samples/css/_config.js b/packages/svelte/tests/sourcemaps/samples/css/_config.js
index df3c83c703..fce38d401c 100644
--- a/packages/svelte/tests/sourcemaps/samples/css/_config.js
+++ b/packages/svelte/tests/sourcemaps/samples/css/_config.js
@@ -1,5 +1,5 @@
import { test } from '../../test';
export default test({
- css: [{ str: '.foo', strGenerated: '.foo.svelte-sg04hs' }]
+ css: [{ str: '.foo', strGenerated: '.foo.svelte-1eyw86p' }]
});
diff --git a/packages/svelte/tests/validator/samples/a11y-anchor-has-content/warnings.json b/packages/svelte/tests/validator/samples/a11y-anchor-has-content/warnings.json
index cd3778a443..3d9adc1cde 100644
--- a/packages/svelte/tests/validator/samples/a11y-anchor-has-content/warnings.json
+++ b/packages/svelte/tests/validator/samples/a11y-anchor-has-content/warnings.json
@@ -1,7 +1,7 @@
[
{
"code": "a11y_consider_explicit_label",
- "message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
+ "message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 1,
"column": 0
diff --git a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte
index e97951065d..463889dc4f 100644
--- a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte
@@ -4,6 +4,9 @@
+
+
+
diff --git a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/warnings.json b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/warnings.json
index 2dcecf08b3..bc29076184 100644
--- a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/warnings.json
+++ b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/warnings.json
@@ -1,7 +1,7 @@
[
{
"code": "a11y_consider_explicit_label",
- "message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
+ "message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 1,
"column": 0
@@ -13,7 +13,7 @@
},
{
"code": "a11y_consider_explicit_label",
- "message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
+ "message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 2,
"column": 0
diff --git a/packages/svelte/tests/validator/samples/binding-dimensions-svg/errors.json b/packages/svelte/tests/validator/samples/binding-dimensions-svg/errors.json
index 4d3819453c..61e6ca6355 100644
--- a/packages/svelte/tests/validator/samples/binding-dimensions-svg/errors.json
+++ b/packages/svelte/tests/validator/samples/binding-dimensions-svg/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
- "message": "`bind:offsetWidth` can only be used with non- elements. Use 'clientWidth' for instead",
+ "message": "`bind:offsetWidth` can only be used with non-`` elements. Use `bind:clientWidth` for `` instead",
"start": {
"line": 5,
"column": 5
diff --git a/packages/svelte/tests/validator/samples/binding-input-checked/errors.json b/packages/svelte/tests/validator/samples/binding-input-checked/errors.json
index 574b25e06f..d7e8e48c95 100644
--- a/packages/svelte/tests/validator/samples/binding-input-checked/errors.json
+++ b/packages/svelte/tests/validator/samples/binding-input-checked/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
- "message": "`bind:checked` can only be used with ",
+ "message": "`bind:checked` can only be used with ` `",
"start": {
"line": 5,
"column": 7
diff --git a/packages/svelte/tests/validator/samples/binding-invalid-on-element-2/errors.json b/packages/svelte/tests/validator/samples/binding-invalid-on-element-2/errors.json
index 92111a4ec2..19d0cd0ce1 100644
--- a/packages/svelte/tests/validator/samples/binding-invalid-on-element-2/errors.json
+++ b/packages/svelte/tests/validator/samples/binding-invalid-on-element-2/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
- "message": "`bind:open` can only be used with ",
+ "message": "`bind:open` can only be used with ``",
"start": {
"line": 5,
"column": 5
diff --git a/packages/svelte/tests/validator/samples/binding-invalid-on-element/errors.json b/packages/svelte/tests/validator/samples/binding-invalid-on-element/errors.json
index 6626afc1d0..781a8301ea 100644
--- a/packages/svelte/tests/validator/samples/binding-invalid-on-element/errors.json
+++ b/packages/svelte/tests/validator/samples/binding-invalid-on-element/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
- "message": "`bind:value` can only be used with , , ",
+ "message": "`bind:value` can only be used with ` `, ``, ``",
"start": {
"line": 5,
"column": 5
diff --git a/packages/svelte/tests/validator/samples/binding-radio-input-checked/errors.json b/packages/svelte/tests/validator/samples/binding-radio-input-checked/errors.json
new file mode 100644
index 0000000000..beb46affc5
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/binding-radio-input-checked/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "bind_invalid_target",
+ "message": "`bind:checked` can only be used with ` ` — for ` `, use `bind:group`",
+ "start": {
+ "line": 5,
+ "column": 20
+ },
+ "end": {
+ "line": 5,
+ "column": 38
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/binding-radio-input-checked/input.svelte b/packages/svelte/tests/validator/samples/binding-radio-input-checked/input.svelte
new file mode 100644
index 0000000000..67e7b77a3a
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/binding-radio-input-checked/input.svelte
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 97e6f0f5a3..6dc6629faa 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -998,7 +998,7 @@ declare module 'svelte/compiler' {
css?: 'injected' | 'external';
/**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
- * It defaults to returning `svelte-${hash(css)}`.
+ * It defaults to returning `svelte-${hash(filename ?? css)}`.
*
* @default undefined
*/
@@ -2493,7 +2493,7 @@ declare module 'svelte/server' {
}
]
): RenderOutput;
- interface RenderOutput {
+ interface SyncRenderOutput {
/** HTML that goes into the `` */
head: string;
/** @deprecated use `body` instead */
@@ -2502,6 +2502,8 @@ declare module 'svelte/server' {
body: string;
}
+ type RenderOutput = SyncRenderOutput & PromiseLike;
+
export {};
}
@@ -2933,7 +2935,7 @@ declare module 'svelte/types/compiler/interfaces' {
css?: 'injected' | 'external';
/**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
- * It defaults to returning `svelte-${hash(css)}`.
+ * It defaults to returning `svelte-${hash(filename ?? css)}`.
*
* @default undefined
*/
@@ -3169,13 +3171,15 @@ declare namespace $state {
? NonReactive
: T extends { toJSON(): infer R }
? R
- : T extends Array
- ? Array>
- : T extends object
- ? T extends { [key: string]: any }
- ? { [K in keyof T]: Snapshot }
- : never
- : never;
+ : T extends readonly unknown[]
+ ? { [K in keyof T]: Snapshot }
+ : T extends Array
+ ? Array>
+ : T extends object
+ ? T extends { [key: string]: any }
+ ? { [K in keyof T]: Snapshot }
+ : never
+ : never;
/**
* Declares state that is _not_ made deeply reactive — instead of mutating it,
diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html
index 639409b877..d70409ffb6 100644
--- a/playgrounds/sandbox/index.html
+++ b/playgrounds/sandbox/index.html
@@ -12,7 +12,7 @@