Merge branch 'simplify-async' into async-fragments

pull/16542/head
Rich Harris 1 month ago
commit 37d02af888

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remount at any hydration error

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: emit `await_reactivity_loss` in `for await` loops

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: emit `snippet_invalid_export` instead of `undefined_export` for exported snippets

@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi
An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.

@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

@ -22,7 +22,7 @@ It also will not compile Svelte code.
## Styling
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused:
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused:
<!-- prettier-ignore -->
```svelte

@ -12,7 +12,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
- `runes={false}` — forces a component into _legacy mode_
- `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml"
- `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
- `css="injected"` — the component will inject its styles inline: During server-side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
> [!LEGACY] Deprecated options
> Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode.

@ -41,7 +41,7 @@ If a function is returned from `onMount`, it will be called when the component i
</script>
```
> [!NOTE] This behaviour will only work when the function passed to `onMount` _synchronously_ returns a value. `async` functions always return a `Promise`, and as such cannot _synchronously_ return a function.
> [!NOTE] This behaviour will only work when the function passed to `onMount` is _synchronous_. `async` functions always return a `Promise`.
## `onDestroy`

@ -245,7 +245,7 @@ In Svelte 4, you can add event modifiers to handlers:
<button on:click|once|preventDefault={handler}>...</button>
```
Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
Modifiers are specific to `on:` and so do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
Since event handlers are just functions, you can create your own wrappers as necessary:
@ -340,7 +340,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r
## Snippets instead of slots
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets, which are more powerful and flexible, and so slots are deprecated in Svelte 5.
They continue to work, however, and you can pass snippets to a component that uses slots:
@ -599,7 +599,7 @@ Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount`
### Server API changes
Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
Similarly, components no longer have a `render` method when compiled for server-side rendering. Instead, pass the function to `render` from `svelte/server`:
```js
+++import { render } from 'svelte/server';+++
@ -803,7 +803,7 @@ Note that Svelte 5 will also warn if you have a single expression wrapped in quo
### HTML structure is stricter
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this...
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server-side rendering it. For example you could write this...
```svelte
<table>
@ -835,7 +835,7 @@ Assignments to destructured parts of a `@const` declaration are no longer allowe
### :is(...), :has(...), and :where(...) are scoped
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. Some selectors may now therefore be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
@ -964,7 +964,7 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
### Hydration works differently
Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
Svelte 5 makes use of comments during server-side rendering which are used for more robust and efficient hydration on the client. You therefore should not remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
### `onevent` attributes are delegated

@ -679,7 +679,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div>
```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:

@ -1,5 +1,11 @@
# svelte
## 5.37.3
### Patch Changes
- fix: reset attribute cache after setting corresponding property ([#16543](https://github.com/sveltejs/svelte/pull/16543))
## 5.37.2
### Patch Changes

@ -30,7 +30,7 @@ See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md).
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md).
## Supporting Svelte

@ -67,7 +67,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div>
```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:

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

@ -229,7 +229,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@ -248,7 +248,7 @@ declare namespace $effect {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute

@ -529,7 +529,6 @@ export function analyze_component(root, source, options) {
has_global: false
},
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
@ -792,9 +791,15 @@ export function analyze_component(root, source, options) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = analysis.module.scope.get(specifier.local.name);
if (!binding) e.export_undefined(specifier, specifier.local.name);
const name = specifier.local.name;
const binding = analysis.module.scope.get(name);
if (!binding) {
if ([...analysis.snippets].find((snippet) => snippet.expression.name === name)) {
e.snippet_invalid_export(specifier);
} else {
e.export_undefined(specifier, name);
}
}
}
}
}

@ -35,11 +35,6 @@ export function SnippetBlock(node, context) {
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
} else {
const undefined_export = context.state.analysis.undefined_exports.get(name);
if (undefined_export) {
e.snippet_invalid_export(undefined_export);
}
}
node.metadata.can_hoist = can_hoist;

@ -26,6 +26,7 @@ import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
@ -103,6 +104,7 @@ const visitors = {
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
ForOfStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,

@ -0,0 +1,20 @@
/** @import { Expression, ForOfStatement, Pattern, Statement, VariableDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { dev, is_ignored } from '../../../../state.js';
/**
* @param {ForOfStatement} node
* @param {ComponentContext} context
*/
export function ForOfStatement(node, context) {
if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
const argument = /** @type {Expression} */ (context.visit(node.right));
const body = /** @type {Statement} */ (context.visit(node.body));
const right = b.call('$.for_await_track_reactivity_loss', argument);
return b.for_of(left, right, body, true);
}
context.next();
}

@ -65,13 +65,14 @@ export function visit_assignment_expression(node, context, build_assignment) {
statements.push(b.return(rhs));
}
const iife = b.arrow([rhs], b.block(statements));
const iife_is_async =
const async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
const iife = b.arrow([rhs], b.block(statements), async);
const call = b.call(iife, value);
return async ? b.await(call) : call;
}
const sequence = b.sequence(assignments);

@ -95,7 +95,6 @@ export interface ComponentAnalysis extends Analysis {
};
/** @deprecated use `source` from `state.js` instead */
source: string;
undefined_exports: Map<string, Node>;
/**
* Every render tag/component, and whether it could be definitively resolved or not
*/

@ -56,15 +56,6 @@ export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', operator, left, right };
}
/**
* @template T
* @param {T & ESTree.BaseFunction} func
* @returns {T & ESTree.BaseFunction}
*/
export function async(func) {
return { ...func, async: true };
}
/**
* @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression}
@ -214,6 +205,23 @@ export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', declaration };
}
/**
* @param {ESTree.VariableDeclaration | ESTree.Pattern} left
* @param {ESTree.Expression} right
* @param {ESTree.Statement} body
* @param {boolean} [_await]
* @returns {ESTree.ForOfStatement}
*/
export function for_of(left, right, body, _await = false) {
return {
type: 'ForOfStatement',
left,
right,
body,
await: _await
};
}
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params

@ -19,7 +19,7 @@ import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
) {
// @ts-ignore
element[name] = value;
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning);
}

@ -98,7 +98,11 @@ export {
props_id,
with_script
} from './dom/template.js';
export { save, track_reactivity_loss } from './reactivity/async.js';
export {
for_await_track_reactivity_loss,
save,
track_reactivity_loss
} from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export {
async_derived,

@ -119,6 +119,51 @@ export async function track_reactivity_loss(promise) {
};
}
/**
* Used in `for await` loops in DEV, so
* that we can emit `await_reactivity_loss` warnings
* after each `async_iterator` result resolves and
* after the `async_iterator` return resolves (if it runs)
* @template T
* @template TReturn
* @param {Iterable<T> | AsyncIterable<T>} iterable
* @returns {AsyncGenerator<T, TReturn | undefined>}
*/
export async function* for_await_track_reactivity_loss(iterable) {
// This is based on the algorithms described in ECMA-262:
// ForIn/OfBodyEvaluation
// https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
// AsyncIteratorClose
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose
/** @type {AsyncIterator<T, TReturn>} */
// @ts-ignore
const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.();
if (iterator === undefined) {
throw new TypeError('value is not async iterable');
}
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
let normal_completion = false;
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
normal_completion = true;
break;
}
yield value;
}
} finally {
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
if (normal_completion && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
}
}
export function unset_context() {
set_active_effect(null);
set_active_reaction(null);

@ -136,7 +136,18 @@ export function hydrate(component, options) {
return /** @type {Exports} */ (instance);
} catch (error) {
if (error === HYDRATION_ERROR) {
// re-throw Svelte errors - they are certainly not related to hydration
if (
error instanceof Error &&
error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
) {
throw error;
}
if (error !== HYDRATION_ERROR) {
// eslint-disable-next-line no-console
console.warn('Failed to hydrate: ', error);
}
if (options.recover === false) {
e.hydration_failed();
}
@ -147,9 +158,6 @@ export function hydrate(component, options) {
set_hydrating(false);
return mount(component, options);
}
throw error;
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'snippet_invalid_export',
message:
'An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets',
position: [26, 29]
}
});

@ -0,0 +1,11 @@
<script module>
export { foo }
</script>
<script>
let x = 42;
</script>
{#snippet foo()}
{x}
{/snippet}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
errors: [
'Failed to hydrate: ',
new DOMException("Node can't be inserted in a #text parent.", 'HierarchyRequestError')
]
});

@ -0,0 +1,2 @@
<!--[-->
<main><p>nested</p><!----></main><!--]-->

@ -0,0 +1,7 @@
<script>
import Nested from './Nested.svelte';
</script>
<main>
<Nested />
</main>

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ target, assert }) {
const input = target.querySelector('input');
const button = target.querySelector('button');
assert.equal(input?.step, 'any');
button?.click();
flushSync();
assert.equal(input?.step, '10');
button?.click();
flushSync();
assert.equal(input?.step, 'any');
}
});

@ -0,0 +1,6 @@
<script>
let step = "any";
</script>
<input type="range" {...{step}} />
<button onclick={() => step = step === "any" ? 10 : "any"}>change step</button>

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
html: `<button>a</button><button>b</button><p>pending</p>`,
async test({ assert, target, warnings }) {
await tick();
assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>');
assert.equal(
warnings[0],
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings.length, 2);
}
});

@ -0,0 +1,24 @@
<script>
let values = $state([1, 2]);
async function get_total() {
let total = 0;
for await (const n of values) {
total += n;
}
return total;
}
</script>
<button onclick={() => values[0]++}>a</button>
<button onclick={() => values[1]++}>b</button>
<svelte:boundary>
<h1>{await get_total()}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -3313,7 +3313,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@ -3332,7 +3332,7 @@ declare namespace $effect {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute

Loading…
Cancel
Save