Merge pull request #3 from sveltejs/main

pull
pull/9993/head
adiGuba 3 years ago committed by GitHub
commit 35c3601b3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve props aliasing

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add support for `{@const}` inside snippet block

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve whitespace handling

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve each block fallback handling

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve attribute directive reactivity detection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve $inspect batching

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve readonly prop messaging

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add `gamepadconnected` and `gamepaddisconnected` events

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better handle array property deletion reactivity

@ -11,8 +11,10 @@
"changesets": [
"afraid-moose-matter",
"beige-flies-wash",
"beige-rabbits-shave",
"brave-walls-destroy",
"brown-spoons-boil",
"chatty-cups-drop",
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
@ -24,6 +26,7 @@
"curly-lizards-dream",
"dirty-garlics-design",
"dirty-tips-add",
"dull-mangos-wave",
"early-ads-tie",
"eight-steaks-shout",
"eighty-bikes-camp",
@ -36,6 +39,7 @@
"five-tigers-search",
"flat-melons-protect",
"forty-comics-invent",
"four-flies-hammer",
"fresh-weeks-trade",
"friendly-lies-camp",
"funny-wombats-argue",
@ -45,7 +49,9 @@
"great-icons-retire",
"green-eggs-approve",
"green-hounds-play",
"happy-suits-film",
"healthy-planes-vanish",
"heavy-ears-rule",
"honest-icons-change",
"hungry-dots-fry",
"hungry-tips-unite",
@ -56,12 +62,15 @@
"kind-deers-lay",
"kind-eagles-join",
"large-clouds-carry",
"large-turkeys-deny",
"late-crabs-lay",
"lazy-masks-sit",
"lazy-months-knock",
"lazy-spiders-think",
"lemon-geese-drum",
"light-pens-watch",
"long-crews-return",
"lovely-carpets-lick",
"lovely-items-turn",
"lovely-rules-eat",
"lucky-schools-hang",
@ -81,10 +90,13 @@
"poor-eggs-enjoy",
"poor-seahorses-flash",
"popular-mangos-rest",
"purple-dragons-peel",
"quiet-camels-mate",
"rare-pears-whisper",
"real-guests-do",
"rich-sheep-burn",
"rich-tables-sing",
"rotten-bags-type",
"rotten-buckets-develop",
"selfish-tools-hide",
"serious-socks-cover",
@ -96,6 +108,7 @@
"shiny-baboons-play",
"shiny-shrimps-march",
"slimy-clouds-talk",
"slow-chefs-dream",
"small-papayas-laugh",
"smart-parents-swim",
"soft-clocks-remember",
@ -104,15 +117,18 @@
"sour-rules-march",
"spicy-plums-admire",
"stale-comics-look",
"strong-gifts-smoke",
"strong-lemons-provide",
"sweet-mangos-beg",
"swift-donkeys-perform",
"swift-ravens-hunt",
"swift-seahorses-deliver",
"tall-books-grin",
"tall-shrimps-worry",
"tall-tigers-wait",
"tasty-numbers-perform",
"ten-foxes-repeat",
"ten-peaches-sleep",
"ten-worms-reflect",
"thin-foxes-lick",
"thirty-flowers-sit",
@ -128,6 +144,8 @@
"wet-games-fly",
"wicked-clouds-exercise",
"wicked-doors-train",
"wild-foxes-wonder",
"wise-dancers-hang",
"wise-donkeys-marry",
"witty-camels-warn"
]

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve unstate type definition

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve event delegation handler hoisting

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly reflect readonly proxy marker

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve each block fast-path heuristic

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve html tag svg behaviour

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure class constructor values are proxied

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better support for top-level snippet declarations

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: cleanup each block animations on destroy

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve each block index handling

@ -1,5 +1,57 @@
# svelte
## 5.0.0-next.26
### Patch Changes
- fix: better handle array property deletion reactivity ([#9921](https://github.com/sveltejs/svelte/pull/9921))
- fix: improve event delegation handler hoisting ([#9929](https://github.com/sveltejs/svelte/pull/9929))
## 5.0.0-next.25
### Patch Changes
- fix: improve whitespace handling ([#9912](https://github.com/sveltejs/svelte/pull/9912))
- fix: improve each block fallback handling ([#9914](https://github.com/sveltejs/svelte/pull/9914))
- fix: cleanup each block animations on destroy ([#9917](https://github.com/sveltejs/svelte/pull/9917))
## 5.0.0-next.24
### Patch Changes
- fix: improve props aliasing ([#9900](https://github.com/sveltejs/svelte/pull/9900))
- feat: add support for `{@const}` inside snippet block ([#9904](https://github.com/sveltejs/svelte/pull/9904))
- fix: improve attribute directive reactivity detection ([#9907](https://github.com/sveltejs/svelte/pull/9907))
- fix: improve $inspect batching ([#9902](https://github.com/sveltejs/svelte/pull/9902))
- chore: improve readonly prop messaging ([#9901](https://github.com/sveltejs/svelte/pull/9901))
- fix: better support for top-level snippet declarations ([#9898](https://github.com/sveltejs/svelte/pull/9898))
## 5.0.0-next.23
### Patch Changes
- feat: add `gamepadconnected` and `gamepaddisconnected` events ([#9861](https://github.com/sveltejs/svelte/pull/9861))
- fix: improve unstate type definition ([#9895](https://github.com/sveltejs/svelte/pull/9895))
- fix: correctly reflect readonly proxy marker ([#9893](https://github.com/sveltejs/svelte/pull/9893))
- chore: improve each block fast-path heuristic ([#9855](https://github.com/sveltejs/svelte/pull/9855))
- fix: improve html tag svg behaviour ([#9894](https://github.com/sveltejs/svelte/pull/9894))
- fix: ensure class constructor values are proxied ([#9888](https://github.com/sveltejs/svelte/pull/9888))
- fix: improve each block index handling ([#9889](https://github.com/sveltejs/svelte/pull/9889))
## 5.0.0-next.22
### Patch Changes

@ -53,6 +53,7 @@ export type KeyboardEventHandler<T extends EventTarget> = EventHandler<KeyboardE
export type MouseEventHandler<T extends EventTarget> = EventHandler<MouseEvent, T>;
export type TouchEventHandler<T extends EventTarget> = EventHandler<TouchEvent, T>;
export type PointerEventHandler<T extends EventTarget> = EventHandler<PointerEvent, T>;
export type GamepadEventHandler<T extends EventTarget> = EventHandler<GamepadEvent, T>;
export type UIEventHandler<T extends EventTarget> = EventHandler<UIEvent, T>;
export type WheelEventHandler<T extends EventTarget> = EventHandler<WheelEvent, T>;
export type AnimationEventHandler<T extends EventTarget> = EventHandler<AnimationEvent, T>;
@ -336,6 +337,12 @@ export interface DOMAttributes<T extends EventTarget> {
onlostpointercapture?: PointerEventHandler<T> | undefined | null;
onlostpointercapturecapture?: PointerEventHandler<T> | undefined | null;
// Gamepad Events
'on:gamepadconnected'?: GamepadEventHandler<T> | undefined | null;
ongamepadconnected?: GamepadEventHandler<T> | undefined | null;
'on:gamepaddisconnected'?: GamepadEventHandler<T> | undefined | null;
ongamepaddisconnected?: GamepadEventHandler<T> | undefined | null;
// UI Events
'on:scroll'?: UIEventHandler<T> | undefined | null;
onscroll?: UIEventHandler<T> | undefined | null;

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

@ -333,7 +333,7 @@ const compiler_options = {
/** @satisfies {Errors} */
const const_tag = {
'invalid-const-placement': () =>
`{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
`{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
};
/** @satisfies {Errors} */

@ -121,6 +121,16 @@ function validate_code(code) {
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const interactive_elements = new Set([
'a',
'button',
'iframe',
'embed',
'input',
'select',
'textarea'
]);
/** @type {Record<string, Set<string>>} */
const disallowed_contents = {
li: new Set(['li']),
@ -143,6 +153,10 @@ const disallowed_contents = {
th: new Set(['td', 'th', 'tr'])
};
for (const interactive_element of interactive_elements) {
disallowed_contents[interactive_element] = interactive_elements;
}
// can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`?

@ -193,13 +193,7 @@ export default class Selector {
}
get_amount_class_specificity_increased() {
let count = 0;
for (const block of this.blocks) {
if (block.should_encapsulate) {
count++;
}
}
return count;
return this.blocks.filter((block) => block.should_encapsulate).length;
}
}

@ -166,6 +166,7 @@ function get_delegated_event(node, context) {
return non_hoistable;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail-out if the arguments keyword is used
@ -174,6 +175,15 @@ function get_delegated_event(node, context) {
}
const binding = scope.get(reference);
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return non_hoistable;
}
if (
binding !== null &&
// Bail-out if the the binding is a rest param
@ -188,6 +198,7 @@ function get_delegated_event(node, context) {
) {
return non_hoistable;
}
visited_references.add(reference);
}
return { type: 'hoistable', function: target_function };
}

@ -476,6 +476,7 @@ export const validation = {
grand_parent?.type !== 'SvelteComponent' &&
grand_parent?.type !== 'EachBlock' &&
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {

@ -81,6 +81,9 @@ export function serialize_get_binding(node, state) {
return b.call(node);
}
if (binding.prop_alias) {
return b.member(b.id('$$props'), b.id(binding.prop_alias));
}
return b.member(b.id('$$props'), node);
}
@ -157,16 +160,45 @@ export function serialize_set_binding(node, context, fallback) {
let left = node.left;
// Handle class private/public state assignment cases
while (left.type === 'MemberExpression') {
if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') {
if (context.state.private_state.has(left.property.name) && !state.in_constructor) {
if (
left.object.type === 'ThisExpression' &&
left.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(left.property.name)
) {
const value = get_assignment_value(node, context);
if (state.in_constructor) {
// See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy(value)) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right = b.call('$.proxy', value);
return assignment;
}
}
} else {
return b.call(
'$.set',
left,
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
);
}
} else if (
left.object.type === 'ThisExpression' &&
left.property.type === 'Identifier' &&
context.state.public_state.has(left.property.name) &&
state.in_constructor
) {
const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy(value)) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right = b.call('$.proxy', value);
return assignment;
}
}
}
// @ts-expect-error
left = left.object;
@ -331,8 +363,7 @@ function get_hoistable_params(node, context) {
binding.kind === 'prop' &&
!binding.reassigned &&
binding.initial === null &&
!context.state.analysis.accessors &&
context.state.analysis.runes
!context.state.analysis.accessors
) {
// Handle $$props.something use-cases
if (!added_props) {

@ -63,13 +63,14 @@ function get_attribute_name(element, attribute, context) {
* @param {boolean} is_attributes_reactive
*/
function serialize_style_directives(style_directives, element_id, context, is_attributes_reactive) {
if (style_directives.length > 0) {
const values = style_directives.map((directive) => {
const state = context.state;
for (const directive of style_directives) {
let value =
directive.value === true
? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state)
: serialize_attribute_value(directive.value, context)[1];
return b.stmt(
const grouped = b.stmt(
b.call(
'$.style',
element_id,
@ -80,15 +81,30 @@ function serialize_style_directives(style_directives, element_id, context, is_at
)
)
);
});
const singular = b.stmt(
b.call(
'$.style_effect',
element_id,
b.literal(directive.name),
b.arrow([], value),
/** @type {import('estree').Expression} */ (
directive.modifiers.includes('important') ? b.true : undefined
)
)
);
if (
is_attributes_reactive ||
style_directives.some((directive) => directive.metadata.dynamic)
) {
context.state.update.push(...values.map((v) => ({ grouped: v })));
const contains_call_expression =
Array.isArray(directive.value) &&
directive.value.some(
(v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression
);
if (!is_attributes_reactive && contains_call_expression) {
state.update_effects.push(singular);
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
state.update.push({ grouped, singular });
} else {
context.state.init.push(...values);
state.init.push(grouped);
}
}
}
@ -123,21 +139,21 @@ function parse_directive_name(name) {
* @param {boolean} is_attributes_reactive
*/
function serialize_class_directives(class_directives, element_id, context, is_attributes_reactive) {
if (class_directives.length > 0) {
const values = class_directives.map((directive) => {
const value = /** @type {import('estree').Expression} */ (
context.visit(directive.expression)
const state = context.state;
for (const directive of class_directives) {
const value = /** @type {import('estree').Expression} */ (context.visit(directive.expression));
const grouped = b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value));
const singular = b.stmt(
b.call('$.class_toggle_effect', element_id, b.literal(directive.name), b.arrow([], value))
);
return b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value));
});
const contains_call_expression = directive.expression.type === 'CallExpression';
if (
is_attributes_reactive ||
class_directives.some((directive) => directive.metadata.dynamic)
) {
context.state.update.push(...values.map((v) => ({ grouped: v })));
if (!is_attributes_reactive && contains_call_expression) {
state.update_effects.push(singular);
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
state.update.push({ grouped, singular });
} else {
context.state.init.push(...values);
state.init.push(grouped);
}
}
}
@ -295,7 +311,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
}
is_reactive ||= attribute.metadata.dynamic;
is_reactive ||=
attribute.metadata.dynamic ||
(attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression);
}
const lowercase_attributes =
@ -2137,11 +2155,10 @@ export const template_visitors = {
}
// The runtime needs to know what kind of each block this is in order to optimize for the
// immutable + key==entry case. In that case, the item doesn't need to be reactive, because
// the array as a whole is immutable, so if something changes, it either has to recreate the
// array or use nested reactivity through runes.
// TODO this feels a bit "hidden performance boost"-style, investigate if there's a way
// to make this apply in more cases
// key === item (we avoid extra allocations). In that case, the item doesn't need to be reactive.
// We can guarantee this by knowing that in order for the item of the each block to change, they
// would need to mutate the key/item directly in the array. Given that in runes mode we use ===
// equality, we can apply a fast-path (as long as the index isn't reactive).
let each_type = 0;
if (
@ -2149,21 +2166,22 @@ export const template_visitors = {
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index)
) {
each_type |= EACH_KEYED;
// If there's a destructuring, then we likely need the generated $$index
if (node.index || node.context.type !== 'Identifier') {
each_type |= EACH_INDEX_REACTIVE;
}
if (
context.state.analysis.runes &&
node.key.type === 'Identifier' &&
node.context.type === 'Identifier' &&
node.context.name === node.key.name &&
context.state.options.immutable
(each_type & EACH_INDEX_REACTIVE) === 0
) {
// Fast-path
// Fast-path for when the key === item
each_item_is_reactive = false;
} else {
each_type |= EACH_ITEM_REACTIVE;
}
// If there's a destructuring, then we likely need the generated $$index
if (node.index || node.context.type !== 'Identifier') {
each_type |= EACH_INDEX_REACTIVE;
}
} else {
each_type |= EACH_ITEM_REACTIVE;
}
@ -2236,6 +2254,12 @@ export const template_visitors = {
const item = b.id(each_node_meta.item_name);
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = each_item_is_reactive ? b.call('$.unwrap', item) : item;
if (node.index) {
const index_binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(node.index)
);
index_binding.expression = each_item_is_reactive ? b.call('$.unwrap', index) : index;
}
/** @type {import('estree').Statement[]} */
const declarations = [];
@ -2289,9 +2313,9 @@ export const template_visitors = {
)
: b.literal(null);
const key_function =
node.key && (each_type & 1) /* EACH_ITEM_REACTIVE */ !== 0
node.key && ((each_type & EACH_ITEM_REACTIVE) !== 0 || context.state.options.dev)
? b.arrow(
[node.context.type === 'Identifier' ? node.context : b.id('$$item')],
[node.context.type === 'Identifier' ? node.context : b.id('$$item'), index],
b.block(
declarations.concat(
b.return(/** @type {import('estree').Expression} */ (context.visit(node.key)))
@ -2471,7 +2495,14 @@ export const template_visitors = {
body = /** @type {import('estree').BlockStatement} */ (context.visit(node.body));
}
const path = context.path;
// If we're top-level, then we can create a function for the snippet so that it can be referenced
// in the props declaration (default value pattern).
if (path.length === 1 && path[0].type === 'Fragment') {
context.state.init.push(b.function_declaration(node.expression, args, body));
} else {
context.state.init.push(b.const(node.expression, b.arrow(args, body)));
}
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
}

@ -177,8 +177,9 @@ export function clean_nodes(
node.data = node.data.replace(regex_whitespaces_strict, ' ');
node.raw = node.raw.replace(regex_whitespaces_strict, ' ');
if (
(last_text === null || !regex_ends_with_whitespaces.test(last_text.data)) &&
(!can_remove_entirely || node.data !== ' ')
(last_text === null && !can_remove_entirely) ||
node.data !== ' ' ||
node.data.charCodeAt(0) === 160 // non-breaking space
) {
trimmed.push(node);
}

@ -119,6 +119,14 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
current_fallback = fallback;
};
/** @param {import('./types.js').EachBlock} block */
const clear_each = (block) => {
const flags = block.f;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
const anchor_node = block.a;
reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys);
};
const each = render_effect(
() => {
/** @type {V[]} */
@ -137,7 +145,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
if (fallback_fn !== null) {
if (length === 0) {
if (block.v.length !== 0 || render === null) {
clear_each(block);
create_fallback_effect();
return;
}
} else if (block.v.length === 0 && current_fallback !== null) {
const fallback = current_fallback;
@ -160,17 +170,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
false
);
render = render_effect(
/** @param {import('./types.js').EachBlock} block */
(block) => {
const flags = block.f;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
const anchor_node = block.a;
reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys);
},
block,
true
);
render = render_effect(clear_each, block, true);
push_destroy_fn(each, () => {
const flags = block.f;
@ -744,18 +744,28 @@ export function destroy_each_item_block(
const transitions = block.s;
if (apply_transitions && transitions !== null) {
// We might have pending key transitions, if so remove them first
for (let other of transitions) {
if (other.r === 'key') {
transitions.delete(other);
}
}
if (transitions.size === 0) {
block.s = null;
} else {
trigger_transitions(transitions, 'out');
if (transition_block !== null) {
transition_block.push(block);
}
} else {
return;
}
}
const dom = block.d;
if (!controlled && dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
}
}
/**
* @template V

@ -7,7 +7,8 @@ import {
source,
updating_derived,
UNINITIALIZED,
mutable_source
mutable_source,
batch_inspect
} from '../runtime.js';
import {
define_property,
@ -100,12 +101,12 @@ function unwrap(value, already_unwrapped = new Map()) {
}
/**
* @template {StateObject} T
* @template T
* @param {T} value
* @returns {Record<string | symbol, any>}
* @returns {T}
*/
export function unstate(value) {
return unwrap(value);
return /** @type {T} */ (unwrap(/** @type {StateObject} */ (value)));
}
/**
@ -139,18 +140,34 @@ const handler = {
deleteProperty(target, prop) {
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
const is_array = metadata.a;
const boolean = delete target[prop];
// If we have mutated an array directly, and the deletion
// was successful we will also need to update the length
// before updating the field or the version. This is to
// ensure any effects observing length can execute before
// effects that listen to the fields otherwise they will
// operate an an index that no longer exists.
if (is_array && boolean) {
const ls = metadata.s.get('length');
const length = target.length - 1;
if (ls !== undefined && ls.v !== length) {
set(ls, length);
}
}
if (s !== undefined) set(s, UNINITIALIZED);
if (prop in target) update(metadata.v);
return delete target[prop];
return boolean;
},
get(target, prop, receiver) {
if (DEV && prop === READONLY_SYMBOL) return target[READONLY_SYMBOL];
if (DEV && prop === READONLY_SYMBOL) {
return Reflect.get(target, READONLY_SYMBOL);
}
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
@ -165,8 +182,17 @@ const handler = {
metadata.s.set(prop, s);
}
const value = s !== undefined ? get(s) : Reflect.get(target, prop, receiver);
if (s !== undefined) {
const value = get(s);
return value === UNINITIALIZED ? undefined : value;
}
if (DEV) {
if (typeof target[prop] === 'function' && prop !== Symbol.iterator) {
return batch_inspect(target, prop, receiver);
}
}
return Reflect.get(target, prop, receiver);
},
getOwnPropertyDescriptor(target, prop) {
@ -184,6 +210,9 @@ const handler = {
},
has(target, prop) {
if (DEV && prop === READONLY_SYMBOL) {
return Reflect.has(target, READONLY_SYMBOL);
}
if (prop === STATE_SYMBOL) {
return true;
}

@ -42,7 +42,7 @@ export function readonly(value) {
*/
const readonly_error = (_, prop) => {
throw new Error(
`Non-bound props cannot be mutated — use \`bind:<prop>={...}\` to make \`${prop}\` settable. Fallback values can never be mutated.`
`Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
);
};

@ -89,6 +89,8 @@ export function reconcile_html(dom, value, svg) {
}
var clone = content.cloneNode(true);
frag_nodes = Array.from(clone.childNodes);
target.before(svg ? /** @type {Node} */ (clone.firstChild) : clone);
frag_nodes.forEach((node) => {
target.before(node);
});
return /** @type {Array<Text | Comment | Element>} */ (frag_nodes);
}

@ -444,6 +444,20 @@ export function class_toggle(dom, class_name, value) {
dom.classList.remove(class_name);
}
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {() => boolean} value
* @returns {void}
*/
export function class_toggle_effect(dom, class_name, value) {
render_effect(() => {
const string = value();
class_toggle(dom, class_name, string);
});
}
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
* @template V
@ -2359,13 +2373,31 @@ export function set_custom_element_data(node, prop, value) {
* @param {boolean} [important]
*/
export function style(dom, key, value, important) {
const style = dom.style;
const prev_value = style.getPropertyValue(key);
if (value == null) {
dom.style.removeProperty(key);
} else {
dom.style.setProperty(key, value, important ? 'important' : '');
if (prev_value !== '') {
style.removeProperty(key);
}
} else if (prev_value !== value) {
style.setProperty(key, value, important ? 'important' : '');
}
}
/**
* @param {HTMLElement} dom
* @param {string} key
* @param {() => string} value
* @param {boolean} [important]
* @returns {void}
*/
export function style_effect(dom, key, value, important) {
render_effect(() => {
const string = value();
style(dom, key, string, important);
});
}
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.

@ -37,6 +37,8 @@ let current_scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
let is_task_queued = false;
// Used for $inspect
export let is_batching_effect = false;
// Handle effect queues
@ -62,8 +64,8 @@ let current_dependencies = null;
let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null;
// Handling capturing of signals from object property getters
let current_should_capture_signal = false;
/** @type {null | import('./types.js').Signal} */
let last_inspected_signal = null;
/** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false;
/** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */
@ -110,6 +112,29 @@ function is_runes(context) {
return component_context !== null && component_context.r;
}
/**
* @param {import("./proxy/proxy.js").StateObject} target
* @param {string | symbol} prop
* @param {any} receiver
*/
export function batch_inspect(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return function () {
const previously_batching_effect = is_batching_effect;
is_batching_effect = true;
try {
return Reflect.apply(value, receiver, arguments);
} finally {
is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null) {
// @ts-expect-error
for (const fn of last_inspected_signal.inspect) fn();
last_inspected_signal = null;
}
}
};
}
/**
* @param {null | import('./types.js').ComponentContext} context_stack_item
* @returns {void}
@ -1053,11 +1078,15 @@ export function set_signal_value(signal, value) {
// @ts-expect-error
if (DEV && signal.inspect) {
if (is_batching_effect) {
last_inspected_signal = signal;
} else {
// @ts-expect-error
for (const fn of signal.inspect) fn();
}
}
}
}
/**
* @template V

@ -1,4 +1,5 @@
import { untrack } from './runtime.js';
import { EACH_INDEX_REACTIVE } from '../../constants.js';
import { source, untrack } from './runtime.js';
import { is_array } from './utils.js';
/** regex of all html void element names */
@ -65,7 +66,7 @@ export function validate_dynamic_element_tag(tag_fn) {
/**
* @param {() => any} collection
* @param {(item: any) => string} key_fn
* @param {(item: any, index: number) => string} key_fn
* @returns {void}
*/
export function validate_each_keys(collection, key_fn) {
@ -78,7 +79,7 @@ export function validate_each_keys(collection, key_fn) {
: Array.from(maybe_array);
const length = array.length;
for (let i = 0; i < length; i++) {
const key = key_fn(array[i]);
const key = key_fn(array[i], i);
if (keys.has(key)) {
throw new Error(
`Cannot have duplicate keys in a keyed each: Keys at index ${keys.get(

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.22';
export const VERSION = '5.0.0-next.26';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,60 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target, window }) {
const button = target.querySelector('button');
ok(button);
assert.htmlEqual(
target.innerHTML,
`
<button>Remove last</button><div class="list"><label><input type="checkbox">
write
some
docs</label><label><input type="checkbox">
start
writing
JSConf
talk</label><label><input type="checkbox">
buy
some
milk</label><label><input type="checkbox">
mow
the
lawn</label><label><input type="checkbox">
feed
the
turtle</label><label><input type="checkbox">
fix
some
bugs</label></div>`
);
flushSync(() => {
button.click();
});
assert.htmlEqual(
target.innerHTML,
`
<button>Remove last</button><div class="list"><label><input type="checkbox">
write
some
docs</label><label><input type="checkbox">
start
writing
JSConf
talk</label><label><input type="checkbox">
buy
some
milk</label><label><input type="checkbox">
mow
the
lawn</label><label><input type="checkbox">
feed
the
turtle</label></div>`
);
}
});

@ -0,0 +1,28 @@
<script>
import { flip } from 'svelte/animate';
let todos = [
{ id: 1, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' },
{ id: 3, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' }
];
function update() {
todos = [...todos]
todos.pop();
}
</script>
<button on:click={update}>Remove last</button>
<div class="list">
{#each todos as todo (todo.id)}
<label animate:flip>
<input type="checkbox" bind:checked={todo.done} />
{todo.description}
</label>
{/each}
</div>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<div>0</div><div>1</div>`
});

@ -0,0 +1,3 @@
{#each {length: 2} as item, i (`${i}`)}
<div>{i}</div>
{/each}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
compileOptions: { dev: true }, // tests `@validate_store` code generation
html: `<button>clicks:\n0</button>`
});

@ -0,0 +1,12 @@
<script>
import {writable} from 'svelte/store'
let store = writable(0)
</script>
<button onclick={() => {
if(store && $store){
}
}}>
clicks: {$store}
</button>

@ -0,0 +1,14 @@
import { ok, test } from '../../test';
export default test({
test({ assert, target, component }) {
let svg = target.querySelector('svg');
ok(svg);
assert.equal(svg.namespaceURI, 'http://www.w3.org/2000/svg');
assert.htmlEqual(
svg.outerHTML,
'<svg height="24" style="border:1px solid red;" width="24"><path d="M17 11h1a3 3 0 0 1 0 6h-1"></path><path d="M9 12v6"></path><path d="M13 12v6"></path><path d="M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z"></path><path d="M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"></path></svg>'
);
}
});

@ -0,0 +1,7 @@
<script>
let content = '<path d="M17 11h1a3 3 0 0 1 0 6h-1" /><path d="M9 12v6" /><path d="M13 12v6" /><path d="M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z" /><path d="M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8" />'
</script>
<svg width="24" height="24" style="border:1px solid red;">
{@html content}
</svg>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,16 @@
<script>
class Counter {
#count = $state();
constructor(v) {
this.#count = v;
}
get count() {
return this.#count;
}
}
const counter = new Counter({ count: 0 });
</script>
<button on:click={() => counter.count.count++}>{counter.count.count}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,12 @@
<script>
class Counter {
count = $state();
constructor(v) {
this.count = v;
}
}
const counter = new Counter({ count: 0 });
</script>
<button on:click={() => counter.count.count++}>{counter.count.count}</button>

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, component }) {
const [b1] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, '<div>abc</div><button>Toggle</button>');
flushSync(() => {
b1.click();
});
assert.htmlEqual(target.innerHTML, '<div>Fallback</div><button>Toggle</button>');
flushSync(() => {
b1.click();
});
assert.htmlEqual(target.innerHTML, '<div>abc</div><button>Toggle</button>');
}
});

@ -0,0 +1,22 @@
<script>
let data = $state({a:1, b:2, c:3});
let filter = $state(false);
function toggle_filter(){
if(filter) {
filter = false;
data = {a:1, b:2, c:3};
} else {
filter = true;
data = {};
}
}
</script>
<div>
{#each Object.keys(data) as key}
{key}
{:else}
Fallback
{/each}
</div>
<button onclick={toggle_filter}>Toggle</button>

@ -0,0 +1,52 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>push</button><button>pop</button><p>1</p><p>2</p><p>3</p>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>push</button><button>pop</button><p>1</p><p>2</p><p>3</p><p>4</p>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>push</button><button>pop</button><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p>`
);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>push</button><button>pop</button><p>1</p><p>2</p><p>3</p><p>4</p>`
);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>push</button><button>pop</button><p>1</p><p>2</p><p>3</p>`
);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(target.innerHTML, `<button>push</button><button>pop</button><p>1</p><p>2</p>`);
}
});

@ -0,0 +1,15 @@
<script>
let numbers = $state([{id: 1}, {id: 2}, {id: 3}]);
</script>
<button onclick={() => numbers.push({id: numbers.length + 1})}>
push
</button>
<button onclick={() => numbers.pop()}>
pop
</button>
{#each numbers as number}
<p>{number.id}</p>
{/each}

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
html: `<div style="color: red;"></div><div class="red"></div><button>toggle</button`,
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'<div class="blue" style="color: blue;"></div><div class="blue"></div><button>toggle</button>'
);
}
});

@ -0,0 +1,18 @@
<script>
let value = $state('red');
const getValue = () => {
return value;
}
const getClass = () => {
return value === 'blue';
}
const getSpread = () => {
return { class: value };
}
</script>
<div class:blue={getClass()} style:color={getValue()}></div>
<div {...getSpread()}></div>
<button on:click={() => value = 'blue'}>toggle</button>

@ -0,0 +1,5 @@
<script>
let { count: definedCount } = $props();
</script>
<button on:click={() => definedCount++}>{definedCount}</button>

@ -0,0 +1,32 @@
import { test } from '../../test';
export default test({
html: `
<p>0 0 0 0</p>
<button>0</button>
<button>0</button>
<button>0</button>
<button>0</button>
`,
async test({ assert, target, component }) {
const [b1, b2, b3, b4] = target.querySelectorAll('button');
b1.click();
b2.click();
b3.click();
b4.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>1 1 0 0</p>
<button>1</button>
<button>1</button>
<button>1</button>
<button>1</button>
`
);
}
});

@ -0,0 +1,15 @@
<script>
import Counter from './Counter.svelte';
let bound = $state(0);
let bound_nested = $state({count: 0});
let unbound = $state(0);
let unbound_nested = $state({count: 0});
</script>
<p>{bound} {bound_nested.count} {unbound} {unbound_nested.count}</p>
<Counter bind:count={bound} />
<Counter bind:count={bound_nested.count} />
<Counter count={unbound} />
<Counter count={unbound_nested.count} />

@ -15,5 +15,5 @@ export default test({
},
runtime_error:
'Non-bound props cannot be mutated — use `bind:<prop>={...}` to make `count` settable. Fallback values can never be mutated.'
'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:<prop>={...}`. Fallback values can never be mutated.'
});

@ -15,5 +15,5 @@ export default test({
},
runtime_error:
'Non-bound props cannot be mutated — use `bind:<prop>={...}` to make `count` settable. Fallback values can never be mutated.'
'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:<prop>={...}`. Fallback values can never be mutated.'
});

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
assert.htmlEqual(target.innerHTML, '<button>0</button>');
await btn?.click();
assert.htmlEqual(target.innerHTML, '<button>2</button>');
await btn?.click();
assert.htmlEqual(target.innerHTML, '<button>4</button>');
}
});

@ -0,0 +1,10 @@
<script>
let count = $state(0);
</script>
{#snippet counter()}
{@const doubled = count * 2}
<button on:click={() => (count += 1)}>{doubled}</button>
{/snippet}
{@render counter()}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown
},
html: `<p>hello world</p>`
});

@ -0,0 +1,7 @@
<script>
const {children = snippet} = $props();
</script>
{@render children()}
{#snippet snippet()}
<p>hello world</p>
{/snippet}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown
},
html: `A\nB\nC\nD`
});

@ -0,0 +1,5 @@
A
{#snippet snip()}C{/snippet}
B
{@render snip()}
D

@ -0,0 +1,11 @@
<script>
import Component2 from './Component2.svelte';
const {state} = $props();
function render(state) {
return state
}
</script>
<Component2 state={render(state)} />

@ -0,0 +1,5 @@
<script>
const {state} = $props();
</script>
{state}

@ -0,0 +1,13 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button></button>\n[object Object]`);
}
});

@ -0,0 +1,12 @@
<script>
import Component from './Component.svelte';
let state = $state();
</script>
<button onclick={() => {
state = {}
}}></button>
{#if state}
<Component state={state} />
{/if}

@ -1,7 +1,7 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"message": "{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 5, "column": 0 },
"end": { "line": 5, "column": 18 }
}

@ -1,7 +1,7 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"message": "{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 7, "column": 4 },
"end": { "line": 7, "column": 18 }
}

@ -95,7 +95,7 @@ All other features, including stores, are still fully supported in runes mode.
### When is it coming out?
When it's done. The goal is 'sometime later this year'.
When it's done. The goal is sometime in early 2024.
### Should I prepare my code for Svelte 5?

Loading…
Cancel
Save