pull/16821/head
Rich Harris 22 hours ago
commit 449868d3b5

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: depend on reads of deriveds created within reaction (async mode)

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: async `class:` + spread attributes were compiled into sync server-side code

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure tick resolves within a macrotask

@ -23,24 +23,6 @@ export default {
The experimental flag will be removed in Svelte 6.
## Boundaries
Currently, you can only use `await` inside a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet:
```svelte
<svelte:boundary>
<MyApp />
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
@ -99,7 +81,9 @@ let b = $derived(await two());
## Indicating loading states
In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
@ -133,6 +117,24 @@ async function onclick() {
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
## Server-side rendering
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = +++await+++ render(App);
```
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.

@ -1,5 +1,13 @@
# svelte
## 5.39.5
### Patch Changes
- fix: allow `{@html await ...}` and snippets with async content on the server ([#16817](https://github.com/sveltejs/svelte/pull/16817))
- fix: use nginx SSI-compatible comments for `$props.id()` ([#16820](https://github.com/sveltejs/svelte/pull/16820))
## 5.39.4
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.39.4",
"version": "5.39.5",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

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

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';
/**
* @param {AST.SnippetBlock} node
@ -15,6 +16,10 @@ export function SnippetBlock(node, context) {
/** @type {BlockStatement} */ (context.visit(node.body))
);
if (node.body.metadata.has_await) {
fn.body = b.block([create_async_block(fn.body)]);
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;

@ -441,9 +441,13 @@ export function prepare_element_spread(
directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression))
: transform(
/** @type {Expression} */ (context.visit(directive.expression)),
directive.metadata.expression
)
)
);
classes = b.object(properties);
}

@ -365,7 +365,7 @@ export function props_id() {
hydrating &&
hydrate_node &&
hydrate_node.nodeType === COMMENT_NODE &&
hydrate_node.textContent?.startsWith(`#`)
hydrate_node.textContent?.startsWith(`$`)
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();

@ -29,7 +29,7 @@ import * as w from '../warnings.js';
import { async_effect, destroy_effect } from './effects.js';
import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
@ -231,7 +231,7 @@ export function async_derived(fn, location) {
export function user_derived(fn) {
const d = derived(fn);
push_reaction_value(d);
if (!async_mode_flag) push_reaction_value(d);
return d;
}

@ -143,7 +143,7 @@ export function hydrate(component, options) {
e.hydration_failed();
}
// If an error occured above, the operations might not yet have been initialised.
// If an error occurred above, the operations might not yet have been initialised.
init_operations();
clear_text_content(target);

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

@ -448,7 +448,7 @@ export function once(get_value) {
*/
export function props_id(renderer) {
const uid = renderer.global.uid();
renderer.push('<!--#' + uid + '-->');
renderer.push('<!--$' + uid + '-->');
return uid;
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.39.4';
export const VERSION = '5.39.5';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,74 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
// In non-async mode we're not reacting to deriveds read in the same context they're defined in
skip_no_async: true,
test({ assert, target, logs }) {
const [a, b] = target.querySelectorAll('button');
flushSync(() => a?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<p>1/0</p
`
);
flushSync(() => a?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<p>2/0</p
`
);
flushSync(() => b?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<p>2/1</p
`
);
flushSync(() => b?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<p>2/2</p
`
);
assert.deepEqual(logs, [
// init
'a',
'b',
'effect a',
'effect b',
// click a
'a',
'effect a',
// click a
'a',
'effect a',
// click b
'a',
'b',
'effect a',
'effect b',
// click b
'a',
'b',
'effect a',
'effect b'
]);
}
});

@ -0,0 +1,30 @@
<script>
let object = $state.raw({ a: 0, b: 0 });
function a() {
console.log('a');
return object.a;
}
function b() {
console.log('b');
let double = $derived(object.b)
return double;
}
$effect(() => {
object.a;
console.log('effect a');
})
$effect(() => {
const b = $derived(object.b);
b;
console.log('effect b');
})
</script>
<button onclick={() => object = { ...object, a: object.a + 1 }}>a</button>
<button onclick={() => object = { ...object, b: object.b + 1 }}>b</button>
<p>{a()}/{b()}</p>

@ -13,10 +13,11 @@ export default test({
target.innerHTML,
`
<button>increment</button>
<p>1/2</p
<p>1/2</p>
<p>1/2</p>
`
);
assert.deepEqual(logs, [0, 0]);
assert.deepEqual(logs, [0, 0, 0, 0]);
}
});

@ -17,10 +17,14 @@
$effect(() => {
foo = new Foo();
});
let bar = $derived(new Foo());
</script>
<button onclick={() => foo.increment()}>increment</button>
<button onclick={() => {foo.increment(); bar.increment()}}>increment</button>
{#if foo}
<p>{foo.value}/{foo.double}</p>
{/if}
<p>{bar.value}/{bar.double}</p>

@ -0,0 +1 @@
<!--[--><div class="test"></div><div style="color: green;"></div><!--]-->

@ -0,0 +1,2 @@
<div class:test={await true} {...{}}></div>
<div style:color={await "green"} {...{}}></div>

@ -0,0 +1,6 @@
{#snippet foo()}
{@const x = await 'this should work'}
<div>{x}</div>
{/snippet}
{@render foo()}

@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"prepare": "node scripts/create-app-svelte.js",
"dev": "vite --host",
"ssr": "node --conditions=development ./ssr-dev.js",
"dev": "SVELTE_INSPECTOR_OPTIONS=false vite --host",
"ssr": "SVELTE_INSPECTOR_OPTIONS=false node --conditions=development ./ssr-dev.js",
"build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr ssr-prod.js",
"prod": "npm run build && node dist/server/ssr-prod",
"preview": "vite preview",

Loading…
Cancel
Save