Merge branch 'main' into async-another-try

async-another-try
Simon H 5 days ago committed by GitHub
commit c930e89e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep creating state sources for property checks during component init

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: flush anything pending before invoking flushSync callback function

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't overwrite an unchanged spread `value`, preserving incomplete number input

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: track missing state property checks in unowned deriveds

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: read batch-local array on each-block commit

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent hydration mismatch recovery from being intercepted by error boundaries

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: preserve dynamic element connections during hydration

@ -38,7 +38,7 @@ This snippet will be shown when the boundary is first created, and will remain v
The `pending` snippet will _not_ be shown for subsequent async updates — for these, you can use [`$effect.pending()`]($effect#$effect.pending).
> [!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.
> [!NOTE] During server-side rendering, a boundary with a `pending` snippet renders that snippet and ignores the rest of its content. See [Server-side rendering](await-expressions#Server-side-rendering).
### `failed`

@ -70,6 +70,10 @@ let color = type === 'danger' ? 'red' : 'green';
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## `#key` block
While the `#key` block is useful when you want transitions/animations to run when a certain expression changes, it can be very expensive — it unmounts every element/component inside each time, so should be used sparingly. Instead of using `#key` to cause expressions in a child component's `<script>` block to re-run, for example, consider wrapping those expressions in `$derived`.
## Events
Any element attribute starting with `on` is treated as an event listener:
@ -167,7 +171,7 @@ Use `createContext` rather than `setContext` and `getContext`, as it provides ty
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](await-expressions) and [hydratable](hydratable) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
If using version 5.36 or higher, you can use [await expressions](await-expressions) and [hydratable](hydratable) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in the plugin options in `vite.config.js` or in the `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features

@ -677,6 +677,12 @@ Cannot use `$$restProps` in runes mode
`let:` directive at invalid position
```
### let_directive_snippet_conflict
```
Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
```
### mixed_event_handler_syntaxes
```

@ -3,4 +3,6 @@ title: svelte/transition
tags: transitions
---
This module provides built-in transition functions for use with the [`transition:`](transition) directive.
> MODULE: svelte/transition

@ -1,5 +1,55 @@
# svelte
## 5.57.2
### Patch Changes
- fix: don't create async deriveds for a component destroyed while waiting for its top-level `await` ([#18931](https://github.com/sveltejs/svelte/pull/18931))
- fix: prevent untracked derived reads from retaining disconnected dependencies ([#18829](https://github.com/sveltejs/svelte/pull/18829))
- fix: error if `{:else}` is followed by another `{:else}` or `{:else if ...}` ([#18900](https://github.com/sveltejs/svelte/pull/18900))
- fix: avoid tracking SvelteURLSearchParams during URL updates ([#18791](https://github.com/sveltejs/svelte/pull/18791))
- fix: restore hydration state when custom element attribute updates throw ([#18467](https://github.com/sveltejs/svelte/pull/18467))
- fix: attach event handlers that are assigned after a top-level `await` in production ([#18933](https://github.com/sveltejs/svelte/pull/18933))
- fix: flush anything pending before invoking flushSync callback function ([#18878](https://github.com/sveltejs/svelte/pull/18878))
- fix: preserve location information for `await` wrappers ([#18859](https://github.com/sveltejs/svelte/pull/18859))
- fix: keep a reactive assignment with no dependencies when `migrate` cannot turn it into a declaration ([#18817](https://github.com/sveltejs/svelte/pull/18817))
- fix: don't hang `migrate` on a declaration that shares a line with its script tag ([#18816](https://github.com/sveltejs/svelte/pull/18816))
- fix: prevent duplicate subscriptions when reconnecting deriveds ([#18899](https://github.com/sveltejs/svelte/pull/18899))
- fix: recover when an unexpected node replaces a nested boundary comment during hydration ([#18925](https://github.com/sveltejs/svelte/pull/18925))
- fix: print `{#await}` blocks that only have a pending branch ([#18908](https://github.com/sveltejs/svelte/pull/18908))
- fix: don't warn about a redundant `link` role on `<area>` elements without an `href` ([#18872](https://github.com/sveltejs/svelte/pull/18872))
- fix: don't throw `state_unsafe_mutation` when the focused element is removed while `bind:activeElement` is used ([#18921](https://github.com/sveltejs/svelte/pull/18921))
- fix: don't overwrite an unchanged spread `value`, preserving incomplete number input ([#18864](https://github.com/sveltejs/svelte/pull/18864))
- fix: read batch-local array on each-block commit ([#18879](https://github.com/sveltejs/svelte/pull/18879))
- fix: error at compile time when a declaration in a snippet redeclares one of its parameters ([#18842](https://github.com/sveltejs/svelte/pull/18842))
- fix: error when using `let:` directives on a component with a `children` snippet ([#18873](https://github.com/sveltejs/svelte/pull/18873))
- fix: ignore stale boundary reset callbacks ([#18886](https://github.com/sveltejs/svelte/pull/18886))
- fix: handle deferred transitions aborted before initialization ([#18776](https://github.com/sveltejs/svelte/pull/18776))
- fix: prevent hydration mismatch recovery from being intercepted by error boundaries ([#18841](https://github.com/sveltejs/svelte/pull/18841))
- fix: preserve dynamic element connections during hydration ([#18855](https://github.com/sveltejs/svelte/pull/18855))
## 5.57.1
### Patch Changes

@ -279,6 +279,10 @@ The same applies to components:
> `let:` directive at invalid position
## let_directive_snippet_conflict
> Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax

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

@ -1213,6 +1213,16 @@ export function let_directive_invalid_placement(node) {
e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
}
/**
* Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function let_directive_snippet_conflict(node, name) {
e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
}
/**
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
* @param {null | number | NodeLike} node

@ -910,8 +910,10 @@ const instance_script = {
start = /** @type {number} */ (parent.start);
end = /** @type {number} */ (parent.end);
}
while (state.str.original[start] !== '\n') start--;
while (state.str.original[end] !== '\n') end++;
// remove the declaration's whole line, or what there is of it inside the script
const script = /** @type {{ start: number; end: number }} */ (state.analysis.instance.ast);
while (start > script.start && state.str.original[start] !== '\n') start--;
while (end < script.end && state.str.original[end] !== '\n') end++;
state.str.update(start, end, '');
}
},
@ -987,8 +989,11 @@ const instance_script = {
return;
}
let declared = 0;
for (const binding of bindings) {
if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
// a binding this statement declares needs its `let` whether or not it is reassigned
if (ids.includes(binding.node) || (binding.reassigned && expression_ids.length === 0)) {
declared++;
check_rune_binding('state');
const init =
binding.kind === 'state'
@ -1004,7 +1009,13 @@ const instance_script = {
}
}
if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
// the statement goes only once every target has a declaration standing in for it
if (
expression_ids.length === 0 &&
bindings.length > 0 &&
declared === bindings.length &&
bindings.every((b) => b.kind !== 'store_sub')
) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}

@ -532,6 +532,7 @@ function next(parser) {
if (block.type === 'IfBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) e.block_invalid_elseif(start);
if (block.alternate) e.block_duplicate_clause(start, '{:else}');
parser.allow_whitespace();
@ -580,6 +581,7 @@ function next(parser) {
if (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}');
if (block.fallback) e.block_duplicate_clause(start, '{:else}');
parser.allow_whitespace();
parser.eat('}', true);

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { determine_slot } from '../../../utils/slot.js';
/**
* @param {AST.LetDirective} node
@ -21,4 +22,25 @@ export function LetDirective(node, context) {
) {
e.let_directive_invalid_placement(node);
}
// an explicit `children` snippet replaces the default slot content, so the `let:`
// directive would never be provided. if the component has a `slot` attribute, the
// directive applies to the component itself instead, which is fine
if (
(parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf') &&
!determine_slot(parent) &&
parent.fragment.nodes.some(
(child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
)
) {
const { expression } = node;
const pattern =
expression === null || (expression.type === 'Identifier' && expression.name === node.name)
? node.name
: `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
e.let_directive_snippet_conflict(node, pattern);
}
}

@ -173,8 +173,8 @@ export function check_element(node, context) {
current_role === get_implicit_role(node.name, attribute_map) &&
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
// <a role="link" /> is ok because without href the a tag doesn't have a role of link
!(node.name === 'a' && !attribute_map.has('href'))
// <a role="link" /> and <area role="link" /> are ok because without href they don't have a role of link
!((node.name === 'a' || node.name === 'area') && !attribute_map.has('href'))
) {
w.a11y_no_redundant_roles(attribute, current_role);
}

@ -18,7 +18,8 @@ export function AwaitExpression(node, context) {
// in dev, note which values are read inside a reactive expression,
// but don't track them
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
// don't use b.await here, this way we preserve location information
return b.call({ ...node, argument: b.call('$.track_reactivity_loss', argument) });
}
return argument === node.argument ? node : { ...node, argument };

@ -119,8 +119,9 @@ export function build_event_handler(node, metadata, context) {
// local variable can be assigned directly
// except in dev mode where when need $.apply()
// in order to handle warnings.
if (!dev && binding?.declaration_kind !== 'import') {
// in order to handle warnings, or when it is
// assigned after a top-level `await`
if (!dev && binding?.declaration_kind !== 'import' && !binding?.blocker) {
return handler;
}
}

@ -1343,6 +1343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
context.next({ scope: child_scope });
// the parameters and what the body declares end up in the same function
const body_scope = /** @type {Scope} */ (scopes.get(node.body));
for (const [name, binding] of body_scope.declarations) {
if (child_scope.declarations.has(name)) {
e.declaration_duplicate(binding.node, name);
}
}
},
Fragment: (node, context) => {

@ -754,7 +754,9 @@ const svelte_visitors = (comments, state) => ({
if (node.pending) {
context.write('}');
block(context, node.pending, state.preserve_whitespace > 0);
if (node.then || node.catch) {
context.write('{:');
}
} else {
context.write(' ');
}

@ -1,5 +1,11 @@
/** @import { Effect, Reaction, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import {
BOUNDARY_EFFECT,
DESTROYED,
DESTROYING,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
} from '#client/constants';
import {
HYDRATION_ERROR,
HYDRATION_START_ELSE,
@ -26,6 +32,7 @@ import {
hydrate_node,
hydrating,
next,
read_hydration_instruction,
skip_nodes,
set_hydrate_node
} from '../hydration.js';
@ -155,16 +162,18 @@ export class Boundary {
this.#effect = block(() => {
if (hydrating) {
const comment = /** @type {Comment} */ (this.#hydrate_open);
const comment_data = read_hydration_instruction(
/** @type {TemplateNode} */ (this.#hydrate_open)
);
hydrate_next();
const server_rendered_pending = comment.data === HYDRATION_START_ELSE;
const server_rendered_failed = comment.data.startsWith(HYDRATION_START_FAILED);
const server_rendered_pending = comment_data === HYDRATION_START_ELSE;
const server_rendered_failed = comment_data.startsWith(HYDRATION_START_FAILED);
if (server_rendered_failed) {
// Server rendered the failed snippet - hydrate it.
// The serialized error is embedded in the comment: <!--[?<json>-->
const serialized_error = JSON.parse(comment.data.slice(HYDRATION_START_FAILED.length));
const serialized_error = JSON.parse(comment_data.slice(HYDRATION_START_FAILED.length));
this.#hydrate_failed_content(serialized_error);
} else if (server_rendered_pending) {
this.#hydrate_pending_content();
@ -221,6 +230,8 @@ export class Boundary {
var calling_on_error = false;
const reset = () => {
if (this.#is_destroyed()) return;
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
@ -244,6 +255,8 @@ export class Boundary {
};
const invoke_onerror = () => {
if (this.#is_destroyed()) return;
try {
calling_on_error = true;
this.#props.onerror?.(error, reset);
@ -256,6 +269,10 @@ export class Boundary {
return { reset, invoke_onerror };
}
#is_destroyed() {
return (this.#effect.f & (DESTROYED | DESTROYING)) !== 0;
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) return;
@ -264,6 +281,8 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
if (this.#is_destroyed()) return;
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
var handled = false;
@ -462,7 +481,7 @@ export class Boundary {
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.oncommit(() => {
this.#handle_error(error);
if (!this.#is_destroyed()) this.#handle_error(error);
});
} else {
this.#handle_error(error);
@ -498,11 +517,13 @@ export class Boundary {
/** @param {unknown} transformed_error */
const handle_error_result = (transformed_error) => {
if (this.#is_destroyed()) return;
const { reset, invoke_onerror } = this.#create_reset(transformed_error);
invoke_onerror();
if (failed) {
if (failed && !this.#is_destroyed()) {
this.#failed_effect = this.#run(() => {
try {
return branch(() => {
@ -528,6 +549,8 @@ export class Boundary {
};
queue_micro_task(() => {
if (this.#is_destroyed()) return;
// Run the error through the API-level transformError transform (e.g. SvelteKit's handleError)
/** @type {unknown} */
var result;

@ -12,7 +12,7 @@ export function append_styles(anchor, css) {
// Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
effect(() => {
// Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will
// never be connected to the real dom. Therfore walk up to the branch that has created the component
// never be connected to the real dom. Therefore walk up to the branch that has created the component
// whose styles we want to append, and check its node instead. It will be connected by the time we get here.
anchor = active_effect?.parent?.nodes?.start ?? anchor;
var root = anchor.getRootNode();

@ -298,6 +298,7 @@ function set_attributes(
var is_option_element = element.nodeName === OPTION_TAG;
var is_select_element = element.nodeName === SELECT_TAG;
try {
for (var key in prev) {
// don't null our internal $$onX listeners
if (!(key in next) && key[0] + key[1] !== '$$') {
@ -317,7 +318,11 @@ function set_attributes(
var setters = get_setters(element);
if (element.nodeName === INPUT_TAG && 'type' in next && ('value' in next || '__value' in next)) {
if (
element.nodeName === INPUT_TAG &&
'type' in next &&
('value' in next || '__value' in next)
) {
var type = next.type;
if (type !== current.type || (type === undefined && element.hasAttribute('type'))) {
@ -485,10 +490,11 @@ function set_attributes(
}
}
}
} finally {
if (is_hydrating_custom_element) {
set_hydrating(true);
}
}
return current;
}

@ -1,4 +1,4 @@
import { listen } from './shared.js';
import { listen, without_reactive_context } from './shared.js';
/**
* @param {(activeElement: Element | null) => void} update
@ -12,6 +12,7 @@ export function bind_active_element(update) {
return;
}
update(document.activeElement);
// Removing the focused element fires `focusout` while an effect is running
without_reactive_context(() => update(document.activeElement));
});
}

@ -359,9 +359,9 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
aborted = true;
a?.abort();
},
deactivate: () => a.deactivate(),
reset: () => a.reset(),
t: () => a.t()
deactivate: () => a?.deactivate(),
reset: () => a?.reset(),
t: () => a?.t() ?? 1 - t2
};
}

@ -241,7 +241,8 @@ export function proxy(value) {
if (
s !== undefined ||
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
((active_effect !== null || active_reaction !== null) &&
(!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = with_parent(() => {

@ -103,6 +103,11 @@ export function flatten(blockers, sync, async, fn) {
if (blocker_promise) {
blocker_promise.then(() => {
if ((parent.f & DESTROYED) !== 0) {
decrement_pending();
return;
}
restore();
run();
unset_context();

@ -65,11 +65,6 @@ import { set_signal_status, update_derived_status } from './reactivity/status.js
import * as w from './warnings.js';
import { queue_micro_task } from './dom/task.js';
/**
* True if updating in an effect context that is reactive (i.e. not branch/root effects)
*/
let is_updating_effect = false;
export let is_destroying_effect = false;
/** @param {boolean} value */
@ -419,7 +414,7 @@ function remove_reaction(signal, dependency) {
// In a fork it's possible that a derived is executed and gets reactions, then commits, but is
// never re-executed. This is possible when the derived is only executed once in the context
// of a new branch which happens before fork.commit() runs. In this case, the derived still has
// UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
// UNINITIALIZED as its value, and then when it's losing its reactions we need to ensure it stays
// DIRTY so it is reexecuted once someone wants its value again.
if (derived.v !== UNINITIALIZED) {
update_derived_status(derived);
@ -471,10 +466,8 @@ export function update_effect(effect) {
set_signal_status(effect, CLEAN);
var previous_effect = active_effect;
var was_updating_effect = is_updating_effect;
active_effect = effect;
is_updating_effect = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0; // Branch/root effects are not reactive contexts
if (DEV) {
var previous_component_fn = dev_current_component_function;
@ -531,7 +524,6 @@ export function update_effect(effect) {
}
}
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
@ -715,13 +707,12 @@ export function get(signal) {
return value;
}
// connect disconnected deriveds if we are reading them inside an effect,
// or inside another derived that is already connected
// connect disconnected deriveds when reading them inside a connected reaction
var should_connect =
(derived.f & CONNECTED) === 0 &&
!untracking &&
active_reaction !== null &&
(is_updating_effect || (active_reaction.f & CONNECTED) !== 0);
(active_reaction.f & CONNECTED) !== 0;
var is_new = (derived.f & REACTION_RAN) === 0;
@ -844,7 +835,13 @@ function reconnect(derived) {
if (derived.deps === null) return;
for (const dep of derived.deps) {
(dep.reactions ??= []).push(derived);
var reactions = dep.reactions;
if (reactions === null) {
dep.reactions = [derived];
} else if (!includes.call(reactions, derived)) {
reactions.push(derived);
}
if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
unfreeze_derived_effects(/** @type {Derived} */ (dep));

@ -42,7 +42,7 @@ export class SvelteURLSearchParams extends URLSearchParams {
if (!this.#url || this.#updating) return;
this.#updating = true;
const search = this.toString();
const search = super.toString();
this.#url.search = search && `?${search}`;
this.#updating = false;

@ -115,6 +115,24 @@ test('url.searchParams', () => {
cleanup();
});
test('url.searchParams writes do not create a reactive dependency', () => {
const url = new SvelteURL('https://svelte.dev');
let runs = 0;
const cleanup = effect_root(() => {
render_effect(() => {
runs += 1;
url.searchParams.set('foo', 'bar');
});
});
flushSync();
assert.equal(runs, 1);
cleanup();
});
test('url.searchParams.set updates url when duplicate values collapse to the same joined string', () => {
const url = new SvelteURL('https://svelte.dev?a=ab&a=c');
const log: any = [];

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

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'block_duplicate_clause',
message: '{:else} cannot appear more than once within a block',
position: [42, 42]
}
});

@ -0,0 +1,7 @@
{#each items as item}
{item}
{:else}
A
{:else}
B
{/each}

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'block_duplicate_clause',
message: '{:else} cannot appear more than once within a block',
position: [23, 23]
}
});

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ item: x })}`)',
position: [62, 74]
}
});

@ -0,0 +1,7 @@
<script>
import Child from './irrelevant';
</script>
<Child let:item={x}>
{#snippet children()}{x}{/snippet}
</Child>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ value })}`)',
position: [359, 368]
}
});

@ -0,0 +1,23 @@
<script>
import Child from './irrelevant';
import Parent from './irrelevant';
</script>
<!-- ok -->
<Child let:value>
{value}
{#snippet foo()}x{/snippet}
</Child>
<Child>
{#snippet children({ value })}{value}{/snippet}
</Child>
<Parent>
<Child slot="foo" let:value>
{#snippet children()}{value}{/snippet}
</Child>
</Parent>
<!-- invalid -->
<Child let:value>
{#snippet children()}Value is: {value}{/snippet}
</Child>

@ -0,0 +1,6 @@
import { test } from '../../test';
// https://github.com/sveltejs/svelte/issues/18924
export default test({
expect_hydration_error: true
});

@ -0,0 +1 @@
<!--[--><!--[--><div><span></span><!--[--><p>inner</p><!--]--></div><!--]--><!--]-->

@ -0,0 +1,14 @@
<svelte:boundary>
<div>
<svelte:boundary>
<p>inner</p>
{#snippet failed()}
<p>inner failed</p>
{/snippet}
</svelte:boundary>
</div>
{#snippet failed()}
<p>outer failed</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,8 @@
<script>
$: count = 7;
$: other = 8;
let obj = {};
$: obj.x = 1;
</script>
<p>{count} {other} {obj.x}</p>

@ -0,0 +1,14 @@
<script>
import { run } from 'svelte/legacy';
let count = $state(7);
let other = $state(8);
let obj = $state({});
run(() => {
obj.x = 1;
});
</script>
<p>{count} {other} {obj.x}</p>

@ -0,0 +1,2 @@
<script>export let answer</script>
<p>{answer}</p>

@ -0,0 +1,3 @@
<script>
let { answer } = $props();</script>
<p>{answer}</p>

@ -34,3 +34,7 @@
{:catch}
<p>Something went wrong</p>
{/await}
{#await promise}
<p>waiting for the promise to resolve...</p>
{/await}

@ -34,3 +34,7 @@
{:catch}
<p>Something went wrong</p>
{/await}
{#await promise}
<p>waiting for the promise to resolve...</p>
{/await}

@ -0,0 +1,27 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
/** @type {string[]} */
const errors = [];
/** @param {ErrorEvent} event */
const on_error = (event) => {
errors.push(event.message);
event.preventDefault();
};
window.addEventListener('error', on_error);
const button = target.querySelector('button');
ok(button);
button.focus();
button.click();
await new Promise((r) => setTimeout(r, 0));
window.removeEventListener('error', on_error);
assert.equal(errors.length, 0, errors.join('\n'));
assert.equal(target.querySelector('button'), null);
}
});

@ -0,0 +1,10 @@
<script>
let activeElement = $state();
let show = $state(true);
</script>
<svelte:document bind:activeElement />
{#if show}
<button onclick={() => (show = false)}>hide</button>
{/if}

@ -0,0 +1,7 @@
<script>
let { promise } = $props();
const value = await promise;
</script>
<p>{await Promise.resolve(value)}</p>

@ -0,0 +1,31 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [hide, resolve] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
`
<button>hide</button>
<button>resolve</button>
<p>loading...</p>
`
);
hide.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>hide</button>
<button>resolve</button>
`
);
}
});

@ -0,0 +1,20 @@
<script>
import Child from './Child.svelte';
const deferred = Promise.withResolvers();
let show = $state(true);
</script>
<button onclick={() => (show = false)}>hide</button>
<button onclick={() => deferred.resolve(1)}>resolve</button>
<svelte:boundary>
{#if show}
<Child promise={deferred.promise} />
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target, window, logs }) {
await new Promise((r) => setTimeout(r));
await tick();
const button = target.querySelector('button');
const div = target.querySelector('div');
button?.click();
div?.dispatchEvent(new window.MouseEvent('mouseenter'));
window.dispatchEvent(new window.Event('resize'));
assert.deepEqual(logs, ['click', 'mouseenter', 'resize']);
}
});

@ -0,0 +1,19 @@
<script>
// Wait a macrotask to make sure the handlers aren't read before the microtask-Promise.resolve() resolves, masking a bug
await new Promise((r) => setTimeout(r));
function create_handler(name) {
return () => {
console.log(name);
};
}
const onclick = create_handler('click');
const onmouseenter = create_handler('mouseenter');
const onresize = create_handler('resize');
</script>
<svelte:window {onresize} />
<button {onclick}>click</button>
<div {onmouseenter}>hover</div>

@ -0,0 +1,18 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [rerun, discard] = target.querySelectorAll('button');
rerun.click();
await tick();
discard.click();
await tick();
assert.deepEqual(logs, [1, false, 1, 1, 1]);
}
});

@ -0,0 +1,49 @@
<script>
import { fork } from 'svelte';
const init = $state({ x: 1 });
const init_missing = $state({});
const root = $state({ x: 1 });
const teardown = $state({ x: 1 });
const after_await = $state({ x: 1 });
let count = $state(0);
// each object is first checked with `in` while an effect is active but no reaction is
'x' in init;
'y' in init_missing;
$effect.root(() => {
'x' in root;
});
$effect(() => {
count;
return () => {
'x' in teardown;
};
});
await Promise.resolve();
'x' in after_await;
</script>
<button onclick={() => count++}>rerun</button>
<button
onclick={() => {
fork(() => {
init.x = 2;
init_missing.y = 1;
root.x = 2;
teardown.x = 2;
after_await.x = 2;
}).discard();
console.log(init.x, 'y' in init_missing, root.x, teardown.x, after_await.x);
}}
>
discard
</button>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');
button.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>Reproduce</button><p>Deferred outro</p>');
}
});

@ -0,0 +1,24 @@
<script>
import { crossfade } from 'svelte/transition';
let visible = $state(true);
let restore = $state(false);
const [send] = crossfade({});
$effect(() => {
if (restore) visible = true;
});
</script>
<button
onclick={() => {
visible = false;
restore = true;
}}
>
Reproduce
</button>
{#if visible}
<p out:send={{ key: 'item' }}>Deferred outro</p>
{/if}

@ -0,0 +1,50 @@
import { tick } from 'svelte';
import { test } from '../../test';
/** @type {Array<() => void>} */
const resolvers = [];
export default test({
transformError: (error) => new Promise((resolve) => resolvers.push(() => resolve(error))),
async test({ assert, target, logs }) {
const [error, toggle, reset, destroy] = target.querySelectorAll('button');
const paragraph = /** @type {HTMLParagraphElement} */ (target.querySelector('p'));
error.click();
await tick();
resolvers.shift()?.();
await tick();
assert.htmlEqual(paragraph.innerHTML, 'boom');
// A retained reset is inert after its boundary has been destroyed
toggle.click();
await tick();
reset.click();
await tick();
assert.htmlEqual(paragraph.innerHTML, 'boom');
// Resolving an error transform cannot resume a destroyed boundary
toggle.click();
await tick();
error.click();
await tick();
toggle.click();
await tick();
resolvers.shift()?.();
await tick();
assert.htmlEqual(paragraph.innerHTML, 'boom');
// A failed snippet's reset is also inert while the boundary is being destroyed
toggle.click();
await tick();
error.click();
await tick();
resolvers.shift()?.();
await tick();
destroy.click();
await tick();
assert.htmlEqual(paragraph.innerHTML, 'boom,boom');
assert.deepEqual(logs, ['render', 'render', 'render']);
}
});

@ -0,0 +1,47 @@
<script>
let show = $state(true);
let must_throw = $state(false);
let reset;
let element;
let errors = $state([]);
let reset_during_cleanup = false;
function throw_error() {
throw new Error('boom');
}
function toggle() {
must_throw = false;
show = !show;
}
function reset_on_cleanup(_, reset_boundary) {
return {
destroy() {
if (reset_during_cleanup) reset_boundary();
}
};
}
function track_render() {
console.log('render');
}
</script>
<button onclick={() => (must_throw = true)}>error</button>
<button onclick={toggle}>toggle</button>
<button onclick={() => reset()}>reset</button>
<button onclick={() => { reset_during_cleanup = true; toggle(); }}>destroy</button>
<p>{errors.join(',')}</p>
{#if show}
<svelte:boundary onerror={(error, fn) => { errors.push(error.message); reset = fn; }}>
{track_render()}
<input bind:this={element} />
{must_throw ? throw_error() : ''}
{#snippet failed(_, failed_reset)}
<input bind:this={element} use:reset_on_cleanup={failed_reset} />
{/snippet}
</svelte:boundary>
{/if}

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>failed: setter error</p><p>after</p>');
}
});

@ -0,0 +1,22 @@
<script module>
if (!customElements.get('throwing-element')) {
customElements.define(
'throwing-element',
class extends HTMLElement {
set value(_) {
throw new Error('setter error');
}
}
);
}
</script>
<svelte:boundary>
<throwing-element {...{ value: 'boom' }}></throwing-element>
{#snippet failed(error)}
<p>failed: {error.message}</p>
{/snippet}
</svelte:boundary>
<p>after</p>

@ -15,7 +15,7 @@ import { proxy } from '../../src/internal/client/proxy';
import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
import { DESTROYED } from '../../src/internal/client/constants';
import { CONNECTED, DESTROYED } from '../../src/internal/client/constants';
import { noop } from 'svelte/internal/client';
import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags';
@ -153,6 +153,46 @@ describe('signals', () => {
};
});
test('unowned deriveds track missing property checks on proxies', () => {
const value = proxy<Record<string, boolean>>({});
return () => {
const has_x = derived(() => 'x' in value);
const owns_y = derived(() => Object.hasOwn(value, 'y'));
assert.isFalse($.get(has_x));
assert.isFalse($.get(owns_y));
value.x = true;
value.y = true;
assert.isTrue($.get(has_x));
assert.isTrue($.get(owns_y));
delete value.x;
delete value.y;
assert.isFalse($.get(has_x));
assert.isFalse($.get(owns_y));
};
});
test('unowned deriveds track missing array indexes', () => {
const value = proxy<boolean[]>([]);
return () => {
const has_first = derived(() => 0 in value);
assert.isFalse($.get(has_first));
value.push(true);
assert.isTrue($.get(has_first));
value.length = 0;
assert.isFalse($.get(has_first));
};
});
test('derived from state', () => {
const log: number[] = [];
@ -1516,12 +1556,137 @@ describe('signals', () => {
destroy();
// a was spuriously added to s.reactions via is_updating_effect
// a was spuriously added to s.reactions
// even though the entire derived chain was read in an untracked context
assert.equal(s.reactions, null);
};
});
test('untracked derived reads inside effects do not reconnect disconnected dependencies', () => {
return () => {
const source = state({ n: 1, items: [1] });
const data = derived(() => $.get(source));
const items = derived(() => $.get(data).items);
const count = derived(() => Math.max(1, $.get(items).length));
const snapshot = derived(() => ({ n: $.get(data).n, count: $.get(count) }));
const show = state(true);
const trigger = state(0);
let rendered = -1;
let seen: { n: number; count: number } | undefined;
const destroy = effect_root(() => {
render_effect(() => {
if ($.get(show)) {
render_effect(() => {
rendered = $.get(snapshot).count;
});
}
});
render_effect(() => {
$.get(trigger);
seen = $.untrack(() => $.get(snapshot));
});
});
flushSync();
assert.equal(rendered, 1);
flushSync(() => set(show, false));
assert.equal(source.reactions, null);
flushSync(() => set(source, { n: 2, items: [1, 2] }));
flushSync(() => set(trigger, 1));
assert.deepEqual(seen, { n: 2, count: 2 });
assert.equal(source.reactions, null);
assert.equal(items.reactions, null);
assert.equal(count.reactions, null);
assert.equal(items.f & CONNECTED, 0);
assert.equal(count.f & CONNECTED, 0);
flushSync(() => set(show, true));
assert.equal(rendered, 2);
assert.equal(source.reactions?.length, 1);
flushSync(() => set(source, { n: 3, items: [1] }));
assert.equal(rendered, 1);
destroy();
flushSync();
assert.equal(source.reactions, null);
};
});
test('reconnecting deriveds does not duplicate subscriptions', () => {
const enabled = state(false);
const shared = state(1);
let value!: Derived<number>;
const destroy_owner = effect_root(() => {
value = derived(() => ($.get(enabled) ? $.get(shared) : 0));
$.untrack(() => $.get(value));
});
const nested_enabled = state(false);
const nested_shared = state(1);
let inner!: Derived<number>;
let outer!: Derived<number>;
const destroy_nested_owner = effect_root(() => {
inner = derived(() => ($.get(nested_enabled) ? $.get(nested_shared) : 0));
outer = derived(() => $.get(inner));
$.untrack(() => $.get(outer));
});
return () => {
flushSync(() => set(enabled, true));
const destroy_reader = effect_root(() => {
render_effect(() => {
$.get(value);
});
});
const enabled_subscriptions = enabled.reactions?.length;
const shared_subscriptions = shared.reactions?.length;
const enabled_reaction = enabled.reactions?.[0];
const shared_reaction = shared.reactions?.[0];
destroy_reader();
destroy_owner();
assert.equal(enabled_subscriptions, 1);
assert.equal(shared_subscriptions, 1);
assert.equal(enabled_reaction, shared_reaction);
assert.equal(enabled.reactions, null);
assert.equal(shared.reactions, null);
flushSync(() => set(nested_enabled, true));
const destroy_nested_reader = effect_root(() => {
render_effect(() => {
$.get(outer);
});
});
const nested_enabled_subscriptions = nested_enabled.reactions?.length;
const nested_shared_subscriptions = nested_shared.reactions?.length;
const nested_enabled_reaction = nested_enabled.reactions?.[0] as Derived<number> | undefined;
const nested_shared_reaction = nested_shared.reactions?.[0];
const inner_subscriptions = nested_enabled_reaction?.reactions?.length;
destroy_nested_reader();
destroy_nested_owner();
assert.equal(nested_enabled_subscriptions, 1);
assert.equal(nested_shared_subscriptions, 1);
assert.equal(inner_subscriptions, 1);
assert.equal(nested_enabled_reaction, nested_shared_reaction);
assert.equal(nested_enabled.reactions, null);
assert.equal(nested_shared.reactions, null);
assert.equal(nested_enabled_reaction?.reactions, null);
};
});
// https://github.com/sveltejs/svelte/issues/18414
test('a reaction that throws after first-reading a fresh derived does not leak it', () => {
const src = state(0);

@ -43,6 +43,11 @@
<header role="banner"></header>
<footer role="contentinfo"></footer>
<!-- area with href has an implicit role of link -->
<area href="/" role="link" alt="foo" />
<!-- Allowed -->
<!-- menu has same interactive role exceptions as ul/ol -->
<menu role="menu"></menu>
<!-- area without href doesn't have a role of link, same as a without href -->
<area role="link" aria-label="foo" />

@ -454,5 +454,17 @@
"column": 8,
"line": 44
}
},
{
"code": "a11y_no_redundant_roles",
"end": {
"column": 26,
"line": 47
},
"message": "Redundant role 'link'",
"start": {
"column": 15,
"line": 47
}
}
]

@ -0,0 +1,14 @@
[
{
"code": "declaration_duplicate",
"message": "`item` has already been declared",
"start": {
"line": 13,
"column": 8
},
"end": {
"line": 13,
"column": 12
}
}
]

@ -0,0 +1,17 @@
<!-- this is ok -->
{#snippet row1(item)}
{#if item}
{const item = 1}
<p>{item}</p>
{/if}
{/snippet}
{@render row1(5)}
<!-- this is not -->
{#snippet row2(item)}
{const item = 1}
<p>{item}</p>
{/snippet}
{@render row2(5)}
Loading…
Cancel
Save