Merge branch 'main' into best-practices-doc

best-practices-doc
Paolo Ricciuti 4 days ago committed by GitHub
commit 07777057de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: proactively defer effects in pending boundary

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: locate Rollup annontaion friendly to JS downgraders

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: detect and error on non-idempotent each block keys in dev mode

@ -62,6 +62,14 @@ Keyed each block has duplicate key at indexes %a% and %b%
Keyed each block has duplicate key `%value%` at indexes %a% and %b%
```
### each_key_volatile
```
Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
```
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
### effect_in_teardown
```

@ -1,5 +1,17 @@
# svelte
## 5.51.3
### Patch Changes
- fix: prevent event delegation logic conflicting between svelte instances ([#17728](https://github.com/sveltejs/svelte/pull/17728))
- fix: treat CSS attribute selectors as case-insensitive for HTML enumerated attributes ([#17712](https://github.com/sveltejs/svelte/pull/17712))
- fix: locate Rollup annontaion friendly to JS downgraders ([#17724](https://github.com/sveltejs/svelte/pull/17724))
- fix: run effects in pending snippets ([#17719](https://github.com/sveltejs/svelte/pull/17719))
## 5.51.2
### Patch Changes

@ -42,6 +42,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Keyed each block has duplicate key `%value%` at indexes %a% and %b%
## each_key_volatile
> Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
## effect_in_teardown
> `%rune%` cannot be used inside an effect cleanup function

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

@ -22,6 +22,50 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']]
]);
/**
* HTML attributes whose enumerated values are case-insensitive per the HTML spec.
* CSS attribute selectors match these values case-insensitively in HTML documents.
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors HTML spec}
*/
const case_insensitive_attributes = new Set([
'accept-charset',
'autocapitalize',
'autocomplete',
'behavior',
'charset',
'crossorigin',
'decoding',
'dir',
'direction',
'draggable',
'enctype',
'enterkeyhint',
'fetchpriority',
'formenctype',
'formmethod',
'formtarget',
'hidden',
'http-equiv',
'inputmode',
'kind',
'loading',
'method',
'preload',
'referrerpolicy',
'rel',
'rev',
'role',
'rules',
'scope',
'shape',
'spellcheck',
'target',
'translate',
'type',
'valign',
'wrap'
]);
/** @type {Compiler.AST.CSS.Combinator} */
const descendant_combinator = {
type: 'Combinator',
@ -523,7 +567,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
(selector.flags?.includes('i') ?? false) ||
(!selector.flags?.includes('s') &&
case_insensitive_attributes.has(selector.name.toLowerCase()))
)
) {
return false;

@ -1,6 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { flatten, increment_pending } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -10,7 +9,6 @@ import {
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
@ -44,12 +42,7 @@ export function async(node, blockers = [], expressions = [], fn) {
return;
}
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
const decrement_pending = increment_pending();
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
@ -72,8 +65,7 @@ export function async(node, blockers = [], expressions = [], fn) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
decrement_pending();
}
});
}

@ -1,8 +1,6 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BLOCK_EFFECT,
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT,
@ -53,7 +51,7 @@ import { set_signal_status } from '../../reactivity/status.js';
* }} BoundaryProps
*/
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED;
/**
* @param {TemplateNode} node
@ -98,15 +96,10 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
/** @type {TemplateNode | null} */
#pending_anchor = null;
#local_pending_count = 0;
#pending_count = 0;
#pending_count_update_queued = false;
#is_creating_fallback = false;
/** @type {Set<Effect>} */
#dirty_effects = new Set();
@ -142,51 +135,31 @@ export class Boundary {
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.parent = /** @type {Effect} */ (active_effect).b;
this.#children = (anchor) => {
var effect = /** @type {Effect} */ (active_effect);
this.is_pending = !!this.#props.pending;
effect.b = this;
effect.f |= BOUNDARY_EFFECT;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
children(anchor);
};
this.parent = /** @type {Effect} */ (active_effect).b;
this.#effect = block(() => {
if (hydrating) {
const comment = this.#hydrate_open;
const comment = /** @type {Comment} */ (this.#hydrate_open);
hydrate_next();
const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
/** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
if (server_rendered_pending) {
if (comment.data === HYDRATION_START_ELSE) {
this.#hydrate_pending_content();
} else {
this.#hydrate_resolved_content();
if (this.#pending_count === 0) {
this.is_pending = false;
}
}
} else {
var anchor = this.#get_anchor();
try {
this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
this.#render();
}
return () => {
this.#pending_anchor?.remove();
};
}, flags);
if (hydrating) {
@ -206,39 +179,75 @@ export class Boundary {
const pending = this.#props.pending;
if (!pending) return;
this.is_pending = true;
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
var anchor = this.#get_anchor();
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
fragment.append(anchor);
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
if (this.#pending_count === 0) {
this.#anchor.before(fragment);
this.#offscreen_fragment = null;
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.is_pending = false;
this.#resolve();
}
});
}
#get_anchor() {
var anchor = this.#anchor;
#render() {
try {
this.is_pending = this.has_pending_snippet();
this.#pending_count = 0;
this.#local_pending_count = 0;
this.#main_effect = branch(() => {
this.#children(this.#anchor);
});
if (this.is_pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
if (this.#pending_count > 0) {
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
move_effect(this.#main_effect, fragment);
anchor = this.#pending_anchor;
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
this.#pending_effect = branch(() => pending(this.#anchor));
} else {
this.#resolve();
}
} catch (error) {
this.error(error);
}
}
#resolve() {
this.is_pending = false;
// any effects that were previously deferred should be rescheduled —
// after the next traversal (which will happen immediately, due to the
// same update that brought us here) the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
return anchor;
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
}
/**
@ -262,7 +271,8 @@ export class Boundary {
}
/**
* @param {() => Effect | null} fn
* @template T
* @param {() => T} fn
*/
#run(fn) {
var previous_effect = active_effect;
@ -285,20 +295,6 @@ export class Boundary {
}
}
#show_pending_snippet() {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
if (this.#pending_effect === null) {
this.#pending_effect = branch(() => pending(this.#anchor));
}
}
/**
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
@ -317,24 +313,7 @@ export class Boundary {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.is_pending = false;
// any effects that were encountered and deferred during traversal
// should be rescheduled — after the next traversal (which will happen
// immediately, due to the same update that brought us here)
// the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
this.#resolve();
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
@ -383,7 +362,7 @@ export class Boundary {
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
if (!onerror && !failed) {
throw error;
}
@ -423,31 +402,18 @@ export class Boundary {
e.svelte_boundary_reset_onerror();
}
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#local_pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.is_pending = this.has_pending_snippet();
this.#run(() => {
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
this.#render();
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
};
queue_micro_task(() => {
@ -462,10 +428,16 @@ export class Boundary {
if (failed) {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {
return branch(() => {
// errors in `failed` snippets cause the boundary to error again
// TODO Svelte 6: revisit this decision, most likely better to go to parent boundary instead
var effect = /** @type {Effect} */ (active_effect);
effect.b = this;
effect.f |= BOUNDARY_EFFECT;
failed(
this.#anchor,
() => error,
@ -475,8 +447,6 @@ export class Boundary {
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
});
}
@ -484,10 +454,6 @@ export class Boundary {
}
}
export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
export function pending() {
if (active_effect === null) {
e.effect_pending_outside_reaction();

@ -250,6 +250,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var value = array[index];
var key = get_key(value, index);
if (DEV) {
// Check that the key function is idempotent (returns the same value when called twice)
var key_again = get_key(value, index);
if (key !== key_again) {
e.each_key_volatile(String(index), String(key), String(key_again));
}
}
var item = first_run ? null : items.get(key);
if (item) {

@ -11,8 +11,11 @@ import {
set_active_reaction
} from '../../runtime.js';
import { without_reactive_context } from './bindings/shared.js';
import { can_delegate_event } from '../../../../utils.js';
/**
* Used on elements, as a map of event type -> event handler,
* and on events themselves to track which element handled an event
*/
export const event_symbol = Symbol('events');
/** @type {Set<string>} */
@ -177,8 +180,8 @@ export function handle_event_propagation(event) {
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// We check `event_symbol` to skip all nodes below it in case this is a
// parent of the `event_symbol` node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
@ -186,7 +189,7 @@ export function handle_event_propagation(event) {
// without it the variable will be DCE'd and things will
// fail mysteriously in Firefox
// @ts-expect-error is added below
var handled_at = last_propagated_event === event && event.__root;
var handled_at = last_propagated_event === event && event[event_symbol];
if (handled_at) {
var at_idx = path.indexOf(handled_at);
@ -198,7 +201,7 @@ export function handle_event_propagation(event) {
// -> ignore, but set handle_at to document/window so that we're resetting the event
// chain in case someone manually dispatches the same event object again.
// @ts-expect-error
event.__root = handler_element;
event[event_symbol] = handler_element;
return;
}
@ -298,7 +301,7 @@ export function handle_event_propagation(event) {
}
} finally {
// @ts-expect-error is used above
event.__root = handler_element;
event[event_symbol] = handler_element;
// @ts-ignore remove proxy on currentTarget
delete event.currentTarget;
set_active_reaction(previous_reaction);

@ -147,6 +147,25 @@ export function each_key_duplicate(a, b, value) {
}
}
/**
* Keyed each block has key that is not idempotent the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
* @param {string} index
* @param {string} a
* @param {string} b
* @returns {never}
*/
export function each_key_volatile(index, a, b) {
if (DEV) {
const error = new Error(`each_key_volatile\nKeyed each block has key that is not idempotent — the key for item at index ${index} was \`${a}\` but is now \`${b}\`. Keys must be the same each time for a given item\nhttps://svelte.dev/e/each_key_volatile`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_volatile`);
}
}
/**
* `%rune%` cannot be used inside an effect cleanup function
* @param {string} rune

@ -8,7 +8,7 @@ import {
set_component_context,
set_dev_stack
} from '../context.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
@ -224,12 +224,7 @@ export function unset_context() {
export function run(thunks) {
const restore = capture();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
const decrement_pending = increment_pending();
var active = /** @type {Effect} */ (active_effect);
@ -286,10 +281,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
.finally(decrement_pending);
return blockers;
}
@ -300,3 +292,17 @@ export function run(thunks) {
export function wait(blockers) {
return Promise.all(blockers.map((b) => b.promise));
}
export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
return () => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
};
}

@ -18,7 +18,8 @@ import {
EAGER_EFFECT,
HEAD_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT
MANAGED_EFFECT,
REACTION_RAN
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -142,7 +143,7 @@ export class Batch {
#decrement_queued = false;
is_deferred() {
#is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
}
@ -202,7 +203,7 @@ export class Batch {
// log_inconsistent_branches(root);
}
if (this.is_deferred()) {
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
@ -246,9 +247,6 @@ export class Batch {
var effect = root.first;
/** @type {Effect | null} */
var pending_boundary = null;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
@ -256,26 +254,9 @@ export class Batch {
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect);
// Inside a `<svelte:boundary>` with a pending snippet,
// all effects are deferred until the boundary resolves
// (except block/async effects, which run immediately)
if (
async_mode_flag &&
pending_boundary === null &&
(flags & BOUNDARY_EFFECT) !== 0 &&
effect.b?.is_pending
) {
pending_boundary = effect;
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if (
pending_boundary !== null &&
(flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
) {
/** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
} else if ((flags & EFFECT) !== 0) {
effects.push(effect);
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
@ -293,16 +274,15 @@ export class Batch {
}
}
var parent = effect.parent;
effect = effect.next;
while (effect !== null) {
var next = effect.next;
while (effect === null && parent !== null) {
if (parent === pending_boundary) {
pending_boundary = null;
if (next !== null) {
effect = next;
break;
}
effect = parent.next;
parent = parent.parent;
effect = effect.parent;
}
}
}
@ -472,7 +452,7 @@ export class Batch {
queue_micro_task(() => {
this.#decrement_queued = false;
if (!this.is_deferred()) {
if (!this.#is_deferred()) {
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
this.revive();
@ -836,6 +816,19 @@ function depends_on(reaction, sources, checked) {
export function schedule_effect(signal) {
var effect = (last_scheduled_effect = signal);
var boundary = effect.b;
// defer render effects inside a pending boundary
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
if (
boundary?.is_pending &&
(signal.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
(signal.f & REACTION_RAN) === 0
) {
boundary.defer_effect(signal);
return;
}
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
@ -847,13 +840,18 @@ export function schedule_effect(signal) {
is_flushing &&
effect === active_effect &&
(flags & BLOCK_EFFECT) !== 0 &&
(flags & HEAD_EFFECT) === 0
(flags & HEAD_EFFECT) === 0 &&
(flags & REACTION_RAN) !== 0
) {
return;
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
effect.f ^= CLEAN;
}
}

@ -40,7 +40,7 @@ import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js';
@ -111,8 +111,6 @@ export function async_derived(fn, label, location) {
e.async_derived_orphan();
}
var boundary = /** @type {Boundary} */ (parent.b);
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
@ -156,10 +154,7 @@ export function async_derived(fn, label, location) {
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
var decrement_pending = increment_pending();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
@ -208,9 +203,8 @@ export function async_derived(fn, label, location) {
}
}
if (should_suspend) {
boundary.update_pending_count(-1);
batch.decrement(blocking);
if (decrement_pending) {
decrement_pending();
}
};

@ -40,8 +40,8 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, current_batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
import { Batch, schedule_effect } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -376,14 +376,16 @@ export function template_effect(fn, sync = [], async = [], blockers = []) {
* @param {Blocker[]} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
var batch = /** @type {Batch} */ (current_batch);
var is_async = async.length > 0 || blockers.length > 0;
if (is_async) batch.increment(true);
if (async.length > 0 || blockers.length > 0) {
var decrement_pending = increment_pending();
}
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)), false);
if (is_async) batch.decrement(true);
if (decrement_pending) {
decrement_pending();
}
});
}

@ -161,48 +161,6 @@ const listeners = new Map();
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */
var event_handle = (events) => {
for (var i = 0; i < events.length; i++) {
var event_name = events[i];
if (registered_events.has(event_name)) continue;
registered_events.add(event_name);
var passive = is_passive_event(event_name);
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
//
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
for (const node of [target, document]) {
var counts = listeners.get(node);
if (counts === undefined) {
counts = new Map();
listeners.set(node, counts);
}
var count = counts.get(event_name);
if (count === undefined) {
node.addEventListener(event_name, handle_event_propagation, { passive });
counts.set(event_name, 1);
} else {
counts.set(event_name, count + 1);
}
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
var component = undefined;
@ -251,6 +209,49 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
}
);
// Setup event delegation _after_ component is mounted - if an error would happen during mount, it would otherwise not be cleaned up
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */
var event_handle = (events) => {
for (var i = 0; i < events.length; i++) {
var event_name = events[i];
if (registered_events.has(event_name)) continue;
registered_events.add(event_name);
var passive = is_passive_event(event_name);
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
//
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
for (const node of [target, document]) {
var counts = listeners.get(node);
if (counts === undefined) {
counts = new Map();
listeners.set(node, counts);
}
var count = counts.get(event_name);
if (count === undefined) {
node.addEventListener(event_name, handle_event_propagation, { passive });
counts.set(event_name, 1);
} else {
counts.set(event_name, count + 1);
}
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
return () => {
for (var event_name of registered_events) {
for (const node of [target, document]) {

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

@ -0,0 +1,11 @@
form[method="get"].svelte-xyz h1:where(.svelte-xyz) {
color: red;
}
form[method="post"].svelte-xyz h1:where(.svelte-xyz) {
color: blue;
}
input[type="text"].svelte-xyz {
color: green;
}

@ -0,0 +1 @@
<form class="svelte-xyz" method="GET"><h1 class="svelte-xyz">Hello</h1></form> <form class="svelte-xyz" method="POST"><h1 class="svelte-xyz">World</h1></form> <input class="svelte-xyz" type="Text" />

@ -0,0 +1,23 @@
<form method="GET">
<h1>Hello</h1>
</form>
<form method="POST">
<h1>World</h1>
</form>
<input type="Text" />
<style>
form[method="get"] h1 {
color: red;
}
form[method="post"] h1 {
color: blue;
}
input[type="text"] {
color: green;
}
</style>

@ -0,0 +1,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<button>increment</button>
<button>shift</button>
<p>0</p>
`,
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>1</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>resolved</p>
`
);
}
});

@ -0,0 +1,31 @@
<script>
let resolvers = [];
function push(value) {
const deferred = Promise.withResolvers();
resolvers.push(() => deferred.resolve(value));
return deferred.promise;
}
function shift() {
resolvers.shift()?.();
}
let count = $state(0);
</script>
<button onclick={() => count += 1}>
increment
</button>
<button onclick={shift}>
shift
</button>
<svelte:boundary>
<p>{await push('resolved')}</p>
{#snippet pending()}
<p>{count}</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
mode: ['client'],
error: 'each_key_volatile'
});

@ -0,0 +1,10 @@
<script>
let things = $state([
{ group: 'a', id: 1 },
{ group: 'b', id: 2 }
]);
</script>
{#each things as thing ([thing.group, thing.id])}
<p>{thing.group}-{thing.id}</p>
{/each}
Loading…
Cancel
Save