Merge remote-tracking branch 'origin/main' into svelte-custom-renderer

svelte-custom-renderer
paoloricciuti 6 hours ago
commit 0af2ad75b1

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent untracked derived reads from retaining disconnected dependencies

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: restore hydration state when custom element attribute updates throw

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve location information for `await` wrappers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep a reactive assignment with no dependencies when `migrate` cannot turn it into a declaration

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't hang `migrate` on a declaration that shares a line with its script tag

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't warn about a redundant `link` role on `<area>` elements without an `href`

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error at compile time when a declaration in a snippet redeclares one of its parameters

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error when using `let:` directives on a component with a `children` snippet

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ignore stale boundary reset callbacks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle deferred transitions aborted before initialization

@ -683,6 +683,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
```

@ -283,6 +283,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

@ -1223,6 +1223,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

@ -911,8 +911,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, '');
}
},
@ -988,8 +990,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'
@ -1005,7 +1010,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;
}

@ -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 };

@ -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) => {

@ -17,7 +17,7 @@ if (DEV) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
let value; // let's hope noone modifies this global, but belts and braces
let value; // let's hope no one modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return

@ -131,8 +131,10 @@ export function trace(label, fn) {
}
/**
* @param {Value} source
* @template {Value} T
* @param {T} source
* @param {string} label
* @returns {T}
*/
export function tag(source, label) {
source.label = label;

@ -1,5 +1,11 @@
/** @import { Effect, 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,
@ -236,6 +242,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;
@ -259,6 +267,8 @@ export class Boundary {
};
const invoke_onerror = () => {
if (this.#is_destroyed()) return;
try {
calling_on_error = true;
this.#props.onerror?.(error, reset);
@ -271,6 +281,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;
@ -279,6 +293,8 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
if (this.#is_destroyed()) return;
var pop_renderer = push_renderer(this.#effect.r);
try {
@ -488,7 +504,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);
@ -524,11 +540,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(() => {
@ -554,6 +572,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;

@ -1,4 +1,4 @@
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
@ -255,9 +255,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
tag(each_array, '{#each ...}');
}
/** @type {V[]} */
var array;
/** @type {Map<Batch, Set<any>>} */
var pending = new Map();
@ -276,6 +273,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
try {
state.pending.delete(batch);
// The effect doesn't necessarily re-run in a batch right before that batch commits
// (its view of the collection may not have changed), so we read the collection
// as the committing batch sees it rather than using the most recent block run's result
var array = get(each_array);
state.fallback = fallback;
reconcile(state, array, anchor, flags, get_key);
@ -309,7 +311,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
var effect = block(() => {
array = /** @type {V[]} */ (get(each_array));
var array = /** @type {V[]} */ (get(each_array));
var length = array.length;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -709,7 +711,7 @@ function reconcile(state, array, anchor, flags, get_key) {
* @param {V} value
* @param {unknown} key
* @param {number} index
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}

@ -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();

@ -310,6 +310,7 @@ function set_attributes(
var is_option_element = node_name(element) === OPTION_TAG;
var is_select_element = node_name(element) === SELECT_TAG;
try {
for (var key in prev) {
// don't null our internal $$onX listeners
if (!(key in next) && key[0] + key[1] !== '$$') {
@ -524,10 +525,11 @@ function set_attributes(
}
}
}
} finally {
if (is_hydrating_custom_element) {
set_hydrating(true);
}
}
return current;
}

@ -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
};
}

@ -1032,15 +1032,15 @@ export class Batch {
*/
export function flushSync(fn) {
var was_flushing_sync = is_flushing_sync;
var prev_previous_batch = previous_batch;
previous_batch = null;
is_flushing_sync = true;
try {
var result;
if (fn) {
if (current_batch !== null && !current_batch.is_fork) {
current_batch.flush();
}
flushSync(); // flush anything pending through the while loop below
result = fn();
}
@ -1056,6 +1056,7 @@ export function flushSync(fn) {
}
} finally {
is_flushing_sync = was_flushing_sync;
previous_batch = prev_previous_batch;
}
}
@ -1295,6 +1296,10 @@ export function eager(fn) {
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
if (DEV) {
version.label ??= '$state.eager version';
}
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
@ -1402,6 +1407,8 @@ export function fork(fn) {
e.fork_timing();
}
flushSync();
var batch = Batch.ensure();
batch.is_fork = true;
batch_values = new Map();
@ -1409,7 +1416,8 @@ export function fork(fn) {
var committed = false;
var settled = batch.settled();
flushSync(fn);
fn();
flushSync();
return {
commit: async () => {

@ -46,7 +46,7 @@ import { set_signal_status, update_derived_status } from './status.js';
/** @type {Set<Effect>} */
export let eager_effects = new Set();
/** @type {Map<Source, any>} */
/** @type {Map<Value, any>} */
export const old_values = new Map();
/**
@ -141,7 +141,7 @@ export function mutate(source, value) {
/**
* @template V
* @param {Source<V>} source
* @param {Value<V>} source
* @param {V} value
* @param {boolean} [should_proxy]
* @returns {V}
@ -181,7 +181,7 @@ var count_deps = 0;
/**
* @template V
* @param {Source<V>} source
* @param {Value<V>} source
* @param {V} value
* @param {Effect[] | null} [updated_during_traversal]
* @returns {V}

@ -61,11 +61,6 @@ import { set_signal_status, update_derived_status } from './reactivity/status.js
import * as w from './warnings.js';
import { push_renderer } from './custom-renderer/state.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 */
@ -94,7 +89,7 @@ export function set_active_effect(effect) {
/**
* When sources are created within a reaction, reading and writing
* them within that reaction should not cause a re-run
* @type {null | Set<Source>}
* @type {null | Set<Value>}
*/
export let current_sources = null;
@ -122,11 +117,11 @@ export let skipped_deps = 0;
/**
* Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it
* @type {null | Source[]}
* @type {null | Value[]}
*/
export let untracked_writes = null;
/** @param {null | Source[]} value */
/** @param {null | Value[]} value */
export function set_untracked_writes(value) {
untracked_writes = value;
}
@ -411,7 +406,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);
@ -463,10 +458,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
var pop_renderer = push_renderer(effect.r);
@ -501,7 +494,6 @@ export function update_effect(effect) {
}
}
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
pop_renderer?.();
@ -685,13 +677,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;

@ -168,7 +168,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
// value/checked takes precedence over defaultValue/defaultChecked
name = name === 'defaultvalue' ? 'value' : 'checked';
if (name in attrs) continue;
// We're checking prior entries aswell because "name in attrs" is not enough as the attributes may have different casing
// We're checking prior entries as well because "name in attrs" is not enough as the attributes may have different casing
for (let j = 0; j < names.length; j++) {
if (names[j].toLowerCase() === name) continue outer;
}

@ -1,4 +1,4 @@
/** @import { Source } from '#client' */
/** @import { Derived } from '#client' */
import { derived } from '../internal/client/index.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
@ -42,7 +42,7 @@ var inited = false;
export class SvelteDate extends Date {
#time = state(super.getTime());
/** @type {Map<keyof Date, Source<unknown>>} */
/** @type {Map<keyof Date, Derived<unknown>>} */
#deriveds = new Map();
#reaction = active_reaction;

@ -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,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>

@ -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';
@ -1516,12 +1516,68 @@ 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);
};
});
// 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