Merge branch 'main' into bindable-types

pull/11225/head
Simon Holthausen 2 years ago
commit b9ee487b58

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: refine css `:global()` selector checks in a compound selector

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: warn on slots and event handlers in runes mode, error on `<slot>` + `{@render ...}` tag usage in same component

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: fall back to component namespace when not statically determinable, add way to tell `<svelte:element>` the namespace at runtime

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: remove memory leak from bind:this

@ -76,6 +76,7 @@
"dry-clocks-grow",
"dry-eggs-play",
"dry-eggs-retire",
"dry-fans-march",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-mangos-wave",
@ -162,6 +163,7 @@
"hungry-trees-travel",
"itchy-beans-melt",
"itchy-bulldogs-tan",
"itchy-eels-marry",
"itchy-kings-deliver",
"itchy-lions-wash",
"itchy-terms-guess",
@ -279,6 +281,7 @@
"rich-cobras-exist",
"rich-garlics-laugh",
"rich-olives-yell",
"rich-plums-thank",
"rich-sheep-burn",
"rich-tables-sing",
"rich-waves-mix",
@ -308,6 +311,7 @@
"sharp-kids-happen",
"sharp-tomatoes-learn",
"shiny-baboons-play",
"shiny-rats-heal",
"shiny-shrimps-march",
"short-buses-camp",
"short-countries-rush",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take outroing elements out of the flow when animating siblings

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: remove memory leak from retaining old DOM elements

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add warning when using `$bindable` rune without calling it

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: allow inspect reactivity map, set, date

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: widen ownership when sub state is assigned to new state

@ -1,5 +1,17 @@
# svelte
## 5.0.0-next.107
### Patch Changes
- fix: refine css `:global()` selector checks in a compound selector ([#11142](https://github.com/sveltejs/svelte/pull/11142))
- fix: remove memory leak from bind:this ([#11194](https://github.com/sveltejs/svelte/pull/11194))
- fix: remove memory leak from retaining old DOM elements ([#11197](https://github.com/sveltejs/svelte/pull/11197))
- feat: add warning when using `$bindable` rune without calling it ([#11181](https://github.com/sveltejs/svelte/pull/11181))
## 5.0.0-next.106
### Patch Changes

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

@ -108,6 +108,8 @@ const css = {
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-type-selector-placement': () =>
`:global(...) must not be followed with a type selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
@ -161,7 +163,9 @@ const special_elements = {
* @param {string | null} match
*/
'invalid-svelte-tag': (tags, match) =>
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`,
'conflicting-slot-usage': () =>
`Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.`
};
/** @satisfies {Errors} */

@ -99,41 +99,31 @@ const validation_visitors = {
}
}
// ensure `:global(...)`contains a single selector
// (standalone :global() with multiple selectors is OK)
if (node.children.length > 1 || node.children[0].selectors.length > 1) {
for (const relative_selector of node.children) {
for (const selector of relative_selector.selectors) {
if (
selector.type === 'PseudoClassSelector' &&
selector.name === 'global' &&
selector.args !== null &&
selector.args.children.length > 1
) {
error(selector, 'invalid-css-global-selector');
}
}
}
}
// ensure `:global(...)` is not part of a larger compound selector
// ensure `:global(...)` do not lead to invalid css after `:global()` is removed
for (const relative_selector of node.children) {
for (let i = 0; i < relative_selector.selectors.length; i++) {
const selector = relative_selector.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
const child = selector.args?.children[0].children[0];
// ensure `:global(element)` to be at the first position in a compound selector
if (child?.selectors[0].type === 'TypeSelector' && i !== 0) {
error(selector, 'invalid-css-global-selector-list');
}
// ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element`
if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') {
error(relative_selector.selectors[i + 1], 'invalid-css-type-selector-placement');
}
// ensure `:global(...)`contains a single selector
// (standalone :global() with multiple selectors is OK)
if (
child?.selectors[0].type === 'TypeSelector' &&
!/[.:#]/.test(child.selectors[0].name[0]) &&
(i !== 0 ||
relative_selector.selectors
.slice(1)
.some(
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
))
selector.args !== null &&
selector.args.children.length > 1 &&
(node.children.length > 1 || relative_selector.selectors.length > 1)
) {
error(selector, 'invalid-css-global-selector-list');
error(selector, 'invalid-css-global-selector');
}
}
}

@ -379,6 +379,7 @@ export function analyze_component(root, source, options) {
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,
uses_render_tags: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
@ -388,7 +389,7 @@ export function analyze_component(root, source, options) {
!!options.legacy?.componentApi,
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Set(),
slot_names: new Map(),
warnings,
css: {
ast: root.css,
@ -502,6 +503,10 @@ export function analyze_component(root, source, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
}
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
error(analysis.slot_names.values().next().value, 'conflicting-slot-usage');
}
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) {
@ -1087,7 +1092,7 @@ const common_visitors = {
break;
}
}
context.state.analysis.slot_names.add(name);
context.state.analysis.slot_names.set(name, node);
},
StyleDirective(node, context) {
if (node.value === true) {
@ -1336,7 +1341,8 @@ const common_visitors = {
ancestor.type === 'SvelteFragment' ||
ancestor.type === 'SnippetBlock'
) {
// Inside a slot or a snippet -> this resets the namespace, so we can't determine it
// Inside a slot or a snippet -> this resets the namespace, so assume the component namespace
node.metadata.svg = context.state.options.namespace === 'svg';
return;
}
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {

@ -578,6 +578,8 @@ const validation = {
});
},
RenderTag(node, context) {
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') {
@ -1174,6 +1176,27 @@ export const validation_runes = merge(validation, a11y_validators, {
}
}
},
AssignmentPattern(node, { state, path }) {
if (
node.right.type === 'Identifier' &&
node.right.name === '$bindable' &&
!state.scope.get('bindable')
) {
warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration');
}
},
SlotElement(node, { state, path }) {
if (!state.analysis.custom_element) {
warn(state.analysis.warnings, node, path, 'deprecated-slot-element');
}
},
OnDirective(node, { state, path }) {
const parent_type = path.at(-1)?.type;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name);
}
},
// TODO this is a code smell. need to refactor this stuff
ClassBody: validation_runes_js.ClassBody,
ClassDeclaration: validation_runes_js.ClassDeclaration,

@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js';
import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { is_state_source, serialize_get_binding } from './utils.js';
import { serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
/**

@ -2008,6 +2008,9 @@ export const template_visitors = {
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
const attributes = [];
/** @type {import('#compiler').Attribute['value'] | undefined} */
let dynamic_namespace = undefined;
/** @type {import('#compiler').ClassDirective[]} */
const class_directives = [];
@ -2036,6 +2039,9 @@ export const template_visitors = {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) {
dynamic_namespace = attribute.value;
}
attributes.push(attribute);
} else if (attribute.type === 'SpreadAttribute') {
attributes.push(attribute);
@ -2090,23 +2096,16 @@ export const template_visitors = {
}
})
);
context.state.init.push(
b.stmt(
b.call(
'$.element',
context.state.node,
get_tag,
node.metadata.svg === true
? b.true
: node.metadata.svg === false
? b.false
: b.literal(null),
inner.length === 0
? /** @type {any} */ (undefined)
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))
)
)
);
const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false];
if (inner.length > 0) {
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
}
if (dynamic_namespace) {
if (inner.length === 0) args.push(b.id('undefined'));
args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]));
}
context.state.init.push(b.stmt(b.call('$.element', ...args)));
},
EachBlock(node, context) {
const each_node_meta = node.metadata;

@ -230,7 +230,7 @@ const visitors = {
context.state.specificity.bumped = true;
// TODO err... can this happen?
// for any :global() at the middle of compound selector
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);

@ -506,7 +506,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node, scope);
for (const id of extract_identifiers(node.param)) {
state.scope.declare(id, 'normal', 'let');
scope.declare(id, 'normal', 'let');
}
next({ scope });

@ -3,6 +3,7 @@ import type {
Css,
Fragment,
RegularElement,
SlotElement,
SvelteElement,
SvelteNode,
SvelteOptions
@ -61,13 +62,14 @@ export interface ComponentAnalysis extends Analysis {
/** Whether the component uses `$$slots` */
uses_slots: boolean;
uses_component_bindings: boolean;
uses_render_tags: boolean;
custom_element: boolean | SvelteOptions['customElement'];
/** If `true`, should append styles through JavaScript */
inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Set<string>;
slot_names: Map<string, SlotElement>;
css: {
ast: Css.StyleSheet | null;
hash: string;

@ -316,10 +316,10 @@ export interface SvelteElement extends BaseElement {
tag: Expression;
metadata: {
/**
* `true`/`false` if this is definitely (not) an svg element.
* `null` means we can't know statically.
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.
*/
svg: boolean | null;
svg: boolean;
scoped: boolean;
};
}

@ -39,7 +39,9 @@ const runes = {
'derived-iife': () =>
`Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``,
'invalid-props-declaration': () =>
`Component properties are declared using $props() in runes mode. Did you forget to call the function?`
`Component properties are declared using $props() in runes mode. Did you forget to call the function?`,
'invalid-bindable-declaration': () =>
`Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?`
};
/** @satisfies {Warnings} */
@ -231,7 +233,12 @@ const legacy = {
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */
'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``,
'deprecated-slot-element': () =>
`Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.`,
/** @param {string} name */
'deprecated-event-handler': (name) =>
`Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`
};
const block = {

@ -16,3 +16,4 @@ export const EFFECT_RAN = 1 << 13;
export const EFFECT_TRANSPARENT = 1 << 14;
export const STATE_SYMBOL = Symbol('$state');
export const INSPECT_SYMBOL = Symbol('$inspect');

@ -127,8 +127,8 @@ export function add_owner(object, owner, global = false) {
}
/**
* @param {import('#client').ProxyMetadata | null} from
* @param {import('#client').ProxyMetadata} to
* @param {import('#client').ProxyMetadata<any> | null} from
* @param {import('#client').ProxyMetadata<any>} to
*/
export function widen_ownership(from, to) {
if (to.owners === null) {

@ -24,7 +24,8 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';
import { INERT, STATE_SYMBOL } from '../../constants.js';
import { push_template_node } from '../template.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -70,7 +71,7 @@ function pause_effects(items, controlled_anchor, callback) {
parent_node.append(controlled_anchor);
}
run_out_transitions(transitions, () => {
run_out_transitions(transitions, true, () => {
for (var i = 0; i < length; i++) {
destroy_effect(items[i].e);
}
@ -168,10 +169,11 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
break;
}
var child_open = /** @type {Comment} */ (child_anchor);
child_anchor = hydrate_anchor(child_anchor);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
item = create_item(child_open, child_anchor, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
@ -238,8 +240,8 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
/** @type {import('#client').EachItem[]} */
var to_animate = [];
/** @type {Set<import('#client').EachItem>} */
var to_animate = new Set();
/** @type {import('#client').EachItem[]} */
var matched = [];
@ -267,7 +269,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
if (item !== undefined) {
item.a?.measure();
to_animate.push(item);
to_animate.add(item);
}
}
}
@ -278,8 +280,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
item = items.get(key);
if (item === undefined) {
var child_open = /** @type {Text} */ (push_template_node(empty()));
var child_anchor = current ? current.o : anchor;
child_anchor.before(child_open);
prev = create_item(
current ? get_first_child(current) : anchor,
child_open,
child_anchor,
prev,
prev.next,
value,
@ -302,14 +310,17 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
update_item(item, value, i, flags);
}
resume_effect(item.e);
if ((item.e.f & INERT) !== 0) {
resume_effect(item.e);
to_animate.delete(item);
}
if (item !== current) {
if (seen.has(item)) {
if (matched.length < stashed.length) {
// more efficient to move later items to the front
var start = stashed[0];
var local_anchor = get_first_child(start);
var local_anchor = start.o;
var j;
prev = start.prev;
@ -338,7 +349,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
} else {
// more efficient to move earlier items to the back
seen.delete(item);
move(item, current ? get_first_child(current) : anchor);
move(item, current ? current.o : anchor);
link(item.prev, item.next);
link(item, prev.next);
@ -399,20 +410,6 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
}
}
/**
* @param {import('#client').EachItem} item
* @returns {Text | Element | Comment}
*/
function get_first_child(item) {
var current = item.e.dom;
if (is_array(current)) {
return /** @type {Text | Element | Comment} */ (current[0]);
}
return /** @type {Text | Element | Comment} */ (current);
}
/**
* @param {import('#client').EachItem} item
* @param {any} value
@ -434,6 +431,7 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Comment | Text} open
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
@ -444,7 +442,7 @@ function update_item(item, value, index, type) {
* @param {number} flags
* @returns {import('#client').EachItem}
*/
function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
function create_item(open, anchor, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
@ -462,6 +460,7 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
a: null,
// @ts-expect-error
e: null,
o: open,
prev,
next
};
@ -483,6 +482,8 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
* @param {Text | Element | Comment} anchor
*/
function move(item, anchor) {
anchor.before(item.o);
var dom = item.e.dom;
if (dom !== null) {

@ -1,8 +1,30 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
import { get } from '../../runtime.js';
import { current_effect, get } from '../../runtime.js';
import { is_array } from '../../utils.js';
import { hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html, remove } from '../reconciler.js';
import { push_template_node } from '../template.js';
/**
* @param {import('#client').Effect} effect
* @param {(Element | Comment | Text)[]} to_remove
* @returns {void}
*/
function remove_from_parent_effect(effect, to_remove) {
const dom = effect.dom;
if (is_array(dom)) {
for (let i = dom.length - 1; i >= 0; i--) {
if (to_remove.includes(dom[i])) {
dom.splice(i, 1);
break;
}
}
} else if (dom !== null && to_remove.includes(dom)) {
effect.dom = null;
}
}
/**
* @param {Element | Text | Comment} anchor
@ -11,13 +33,19 @@ import { create_fragment_from_html, remove } from '../reconciler.js';
* @returns {void}
*/
export function html(anchor, get_value, svg) {
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
let value = derived(get_value);
render_effect(() => {
var dom = html_to_dom(anchor, get(value), svg);
var dom = html_to_dom(anchor, parent_effect, get(value), svg);
if (dom) {
return () => remove(dom);
return () => {
if (parent_effect !== null) {
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
}
remove(dom);
};
}
});
}
@ -27,11 +55,12 @@ export function html(anchor, get_value, svg) {
* inserts it before the target anchor and returns the new nodes.
* @template V
* @param {Element | Text | Comment} target
* @param {import('#client').Effect | null} effect
* @param {V} value
* @param {boolean} svg
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
function html_to_dom(target, value, svg) {
function html_to_dom(target, effect, value, svg) {
if (hydrating) return hydrate_nodes;
var html = value + '';
@ -49,6 +78,9 @@ function html_to_dom(target, value, svg) {
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
if (effect !== null) {
push_template_node(child, effect);
}
return child;
}
@ -62,5 +94,9 @@ function html_to_dom(target, value, svg) {
target.before(node);
}
if (effect !== null) {
push_template_node(nodes, effect);
}
return nodes;
}

@ -13,6 +13,7 @@ import { is_array } from '../../utils.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { current_effect } from '../../runtime.js';
import { push_template_node } from '../template.js';
/**
* @param {import('#client').Effect} effect
@ -38,11 +39,12 @@ function swap_block_dom(effect, from, to) {
/**
* @param {Comment} anchor
* @param {() => string} get_tag
* @param {boolean | null} is_svg `null` == not statically known
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace
* @returns {void}
*/
export function element(anchor, get_tag, is_svg, render_fn) {
export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
render_effect(() => {
@ -67,22 +69,18 @@ export function element(anchor, get_tag, is_svg, render_fn) {
block(() => {
const next_tag = get_tag() || null;
const ns = get_namespace
? get_namespace()
: is_svg || next_tag === 'svg'
? namespace_svg
: null;
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
// We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet.
const ns =
is_svg || next_tag === 'svg'
? namespace_svg
: is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
? null
: anchor.parentElement?.namespaceURI ?? null;
if (effect) {
if (next_tag === null) {
// start outro
@ -131,6 +129,8 @@ export function element(anchor, get_tag, is_svg, render_fn) {
if (prev_element) {
swap_block_dom(parent_effect, prev_element, element);
prev_element.remove();
} else if (!hydrating) {
push_template_node(element, parent_effect);
}
});
}

@ -1,6 +1,7 @@
import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_task } from '../../task.js';
/**
* @param {any} bound_value
@ -47,7 +48,8 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
});
return () => {
effect(() => {
// We cannot use effects in the teardown phase, we we use a microtask instead.
queue_task(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}

@ -106,7 +106,7 @@ export function animation(element, get_fn, get_params) {
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => {
animation = animate(this.element, options, false, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
@ -169,7 +169,7 @@ export function transition(flags, element, get_fn, get_params) {
if (is_intro) {
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
intro = animate(element, get_options(), false, outro, 1, () => {
dispatch_event(element, 'introend');
intro = current_options = undefined;
});
@ -178,12 +178,12 @@ export function transition(flags, element, get_fn, get_params) {
reset?.();
}
},
out(fn) {
out(fn, position_absolute = false) {
if (is_outro) {
element.inert = true;
dispatch_event(element, 'outrostart');
outro = animate(element, get_options(), intro, 0, () => {
outro = animate(element, get_options(), position_absolute, intro, 0, () => {
dispatch_event(element, 'outroend');
outro = current_options = undefined;
fn?.();
@ -229,12 +229,13 @@ export function transition(flags, element, get_fn, get_params) {
* Animates an element, according to the provided configuration
* @param {Element} element
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
* @param {boolean} position_absolute
* @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value `1` for intro, `0` for outro
* @param {(() => void) | undefined} callback
* @returns {import('#client').Animation}
*/
function animate(element, options, counterpart, t2, callback) {
function animate(element, options, position_absolute, counterpart, t2, callback) {
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
@ -244,7 +245,7 @@ function animate(element, options, counterpart, t2, callback) {
effect(() => {
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
a = animate(element, o, counterpart, t2, callback);
a = animate(element, o, position_absolute, counterpart, t2, callback);
});
// ...but we want to do so without using `async`/`await` everywhere, so
@ -284,6 +285,9 @@ function animate(element, options, counterpart, t2, callback) {
/** @type {import('#client').Task} */
var task;
/** @type {null | { position: string, width: string, height: string }} */
var original_styles = null;
if (css) {
// WAAPI
var keyframes = [];
@ -295,6 +299,37 @@ function animate(element, options, counterpart, t2, callback) {
keyframes.push(css_to_keyframe(styles));
}
if (position_absolute) {
// we take the element out of the flow, so that sibling elements with an `animate:`
// directive can transform to the correct position
var computed_style = getComputedStyle(element);
if (computed_style.position !== 'absolute' && computed_style.position !== 'fixed') {
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
original_styles = {
position: style.position,
width: style.width,
height: style.height
};
var rect_a = element.getBoundingClientRect();
style.position = 'absolute';
style.width = computed_style.width;
style.height = computed_style.height;
var rect_b = element.getBoundingClientRect();
if (rect_a.left !== rect_b.left || rect_a.top !== rect_b.top) {
var transform = `translate(${rect_a.left - rect_b.left}px, ${rect_a.top - rect_b.top}px)`;
for (var keyframe of keyframes) {
keyframe.transform = keyframe.transform
? `${keyframe.transform} ${transform}`
: transform;
}
}
}
}
animation = element.animate(keyframes, {
delay,
duration,
@ -345,6 +380,15 @@ function animate(element, options, counterpart, t2, callback) {
task?.abort();
},
deactivate: () => {
if (original_styles) {
// revert `animate:` position fixing
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
style.position = original_styles.position;
style.width = original_styles.width;
style.height = original_styles.height;
}
callback = undefined;
},
reset: () => {

@ -1,12 +1,9 @@
import { run_all } from '../../shared/utils.js';
let is_task_queued = false;
let is_raf_queued = false;
/** @type {Array<() => void>} */
let current_queued_tasks = [];
/** @type {Array<() => void>} */
let current_raf_tasks = [];
function process_task() {
is_task_queued = false;
@ -15,11 +12,15 @@ function process_task() {
run_all(tasks);
}
function process_raf_task() {
is_raf_queued = false;
const tasks = current_raf_tasks.slice();
current_raf_tasks = [];
run_all(tasks);
/**
* @param {() => void} fn
*/
export function queue_task(fn) {
if (!is_task_queued) {
is_task_queued = true;
queueMicrotask(process_task);
}
current_queued_tasks.push(fn);
}
/**
@ -29,7 +30,4 @@ export function flush_tasks() {
if (is_task_queued) {
process_task();
}
if (is_raf_queued) {
process_raf_task();
}
}

@ -4,6 +4,32 @@ import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { effect } from '../reactivity/effects.js';
import { is_array } from '../utils.js';
/**
* @param {import("#client").TemplateNode | import("#client").TemplateNode[]} dom
* @param {import("#client").Effect} effect
*/
export function push_template_node(
dom,
effect = /** @type {import('#client').Effect} */ (current_effect)
) {
var current_dom = effect.dom;
if (current_dom === null) {
effect.dom = dom;
} else {
if (!is_array(current_dom)) {
current_dom = effect.dom = [current_dom];
}
if (is_array(dom)) {
current_dom.push(...dom);
} else {
current_dom.push(dom);
}
}
return dom;
}
/**
* @param {string} content
@ -20,15 +46,23 @@ export function template(content, flags) {
return () => {
if (hydrating) {
return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]);
var hydration_content = push_template_node(is_fragment ? hydrate_nodes : hydrate_nodes[0]);
return /** @type {Node} */ (hydration_content);
}
if (!node) {
node = create_fragment_from_html(content);
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
}
var clone = use_import_node ? document.importNode(node, true) : clone_node(node, true);
return use_import_node ? document.importNode(node, true) : clone_node(node, true);
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
: /** @type {import('#client').TemplateNode} */ (clone)
);
return clone;
};
}
@ -71,7 +105,8 @@ export function svg_template(content, flags) {
return () => {
if (hydrating) {
return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]);
var hydration_content = push_template_node(is_fragment ? hydrate_nodes : hydrate_nodes[0]);
return /** @type {Node} */ (hydration_content);
}
if (!node) {
@ -87,7 +122,15 @@ export function svg_template(content, flags) {
}
}
return clone_node(node, true);
var clone = clone_node(node, true);
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
: /** @type {import('#client').TemplateNode} */ (clone)
);
return clone;
};
}
@ -152,7 +195,7 @@ function run_scripts(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function text(anchor) {
if (!hydrating) return empty();
if (!hydrating) return push_template_node(empty());
var node = hydrate_nodes[0];
@ -162,7 +205,7 @@ export function text(anchor) {
anchor.before((node = empty()));
}
return node;
return push_template_node(node);
}
export const comment = template('<!>', TEMPLATE_FRAGMENT);
@ -174,19 +217,7 @@ export const comment = template('<!>', TEMPLATE_FRAGMENT);
* @param {import('#client').Dom} dom
*/
export function append(anchor, dom) {
var current = dom;
if (!hydrating) {
var node = /** @type {Node} */ (dom);
if (node.nodeType === 11) {
// if hydrating, `dom` is already an array of nodes, but if not then
// we need to create an array to store it on the current effect
current = /** @type {import('#client').Dom} */ ([...node.childNodes]);
}
anchor.before(node);
anchor.before(/** @type {Node} */ (dom));
}
/** @type {import('#client').Effect} */ (current_effect).dom = current;
}

@ -38,6 +38,9 @@ export function proxy(value, immutable = true, parent = null) {
// someone copied the state symbol using `Reflect.ownKeys(...)`
if (metadata.t === value || metadata.p === value) {
if (DEV) {
// Since original parent relationship gets lost, we need to copy over ancestor owners
// into current metadata. The object might still exist on both, so we need to widen it.
widen_ownership(metadata, metadata);
metadata.parent = parent;
}

@ -7,9 +7,11 @@ import {
destroy_effect_children,
execute_effect,
get,
is_destroying_effect,
is_flushing_effect,
remove_reactions,
schedule_effect,
set_is_destroying_effect,
set_is_flushing_effect,
set_signal_status,
untrack
@ -109,6 +111,12 @@ export function user_effect(fn) {
(DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '')
);
}
if (is_destroying_effect) {
throw new Error(
'ERR_SVELTE_EFFECT_IN_TEARDOWN' +
(DEV ? ': The Svelte $effect rune can not be used in the teardown phase of an effect.' : '')
);
}
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
@ -140,6 +148,14 @@ export function user_pre_effect(fn) {
: '')
);
}
if (is_destroying_effect) {
throw new Error(
'ERR_SVELTE_EFFECT_IN_TEARDOWN' +
(DEV
? ': The Svelte $effect.pre rune can not be used in the teardown phase of an effect.'
: '')
);
}
return render_effect(fn);
}
@ -228,6 +244,22 @@ export function branch(fn) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true);
}
/**
* @param {import("#client").Effect} effect
*/
export function execute_effect_teardown(effect) {
var teardown = effect.teardown;
if (teardown !== null) {
const previously_destroying_effect = is_destroying_effect;
set_is_destroying_effect(true);
try {
teardown.call(null);
} finally {
set_is_destroying_effect(previously_destroying_effect);
}
}
}
/**
* @param {import('#client').Effect} effect
* @returns {void}
@ -249,7 +281,7 @@ export function destroy_effect(effect) {
}
}
effect.teardown?.call(null);
execute_effect_teardown(effect);
var parent = effect.parent;
@ -302,7 +334,7 @@ export function pause_effect(effect, callback) {
pause_children(effect, transitions, true);
run_out_transitions(transitions, () => {
run_out_transitions(transitions, false, () => {
destroy_effect(effect);
if (callback) callback();
});
@ -310,14 +342,15 @@ export function pause_effect(effect, callback) {
/**
* @param {import('#client').TransitionManager[]} transitions
* @param {boolean} position_absolute
* @param {() => void} fn
*/
export function run_out_transitions(transitions, fn) {
export function run_out_transitions(transitions, position_absolute, fn) {
var remaining = transitions.length;
if (remaining > 0) {
var check = () => --remaining || fn();
for (var transition of transitions) {
transition.out(check);
transition.out(check, position_absolute);
}
} else {
fn();

@ -8,7 +8,12 @@ import {
object_prototype
} from './utils.js';
import { snapshot } from './proxy.js';
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
import {
destroy_effect,
effect,
execute_effect_teardown,
user_pre_effect
} from './reactivity/effects.js';
import {
EFFECT,
RENDER_EFFECT,
@ -22,7 +27,8 @@ import {
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT
ROOT_EFFECT,
INSPECT_SYMBOL
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
@ -37,12 +43,18 @@ let current_scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
export let is_flushing_effect = false;
export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_flushing_effect(value) {
is_flushing_effect = value;
}
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
is_destroying_effect = value;
}
// Used for $inspect
export let is_batching_effect = false;
let is_inspecting_signal = false;
@ -406,7 +418,7 @@ export function execute_effect(effect) {
destroy_effect_children(effect);
}
effect.teardown?.call(null);
execute_effect_teardown(effect);
var teardown = execute_reaction_fn(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
} finally {
@ -658,11 +670,11 @@ export function flush_sync(fn, flush_previous = true) {
var result = fn?.();
flush_tasks();
if (current_queued_root_effects.length > 0 || root_effects.length > 0) {
flush_sync();
}
flush_tasks();
flush_count = 0;
return result;
@ -1106,6 +1118,24 @@ export function pop(component) {
return component || /** @type {T} */ ({});
}
/**
*
* This is called from the inspect.
* Deeply traverse every item in the array with `deep_read` to register for inspect callback
* If the item implements INSPECT_SYMBOL, will use that instead
* @param {Array<any>} value
* @returns {void}
*/
function deep_read_inpect(value) {
for (const item of value) {
if (item && typeof item[INSPECT_SYMBOL] === 'function') {
item[INSPECT_SYMBOL]();
} else {
deep_read(item);
}
}
}
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
@ -1236,7 +1266,7 @@ export function inspect(get_value, inspect = console.log) {
inspect_fn = fn;
const value = get_value();
deep_read(value);
deep_read_inpect(value);
inspect_fn = null;
const signals = inspect_captured_signals.slice();

@ -67,6 +67,8 @@ export type EachItem = {
i: number | Source<number>;
/** key */
k: unknown;
/** anchor for items inserted before this */
o: Comment | Text;
prev: EachItem | EachState;
next: EachItem | null;
};
@ -77,7 +79,7 @@ export interface TransitionManager {
/** Called inside `resume_effect` */
in: () => void;
/** Called inside `pause_effect` */
out: (callback?: () => void) => void;
out: (callback?: () => void, position_absolute?: boolean) => void;
/** Called inside `destroy_effect` */
stop: () => void;
}

@ -1,3 +1,5 @@
import { DEV } from 'esm-env';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
@ -88,6 +90,13 @@ export class ReactiveDate extends Date {
return v;
};
}
if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#raw_time);
};
}
}
}

@ -3,6 +3,9 @@ import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../constants.js';
import { map } from './utils.js';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
var inited = false;
/**
* @template K
@ -20,6 +23,22 @@ export class ReactiveMap extends Map {
constructor(value) {
super();
if (DEV) {
if (!inited) {
inited = true;
// @ts-ignore
ReactiveMap.prototype[INSPECT_SYMBOL] = function () {
// changes could either introduced by
// - modifying the value, or
// - add / remove entries to the map
for (const [, source] of this.#sources) {
get(source);
}
get(this.#size);
};
}
}
// If the value is invalid then the native exception will fire here
if (DEV) new Map(value);
@ -28,7 +47,6 @@ export class ReactiveMap extends Map {
for (var [key, v] of value) {
sources.set(key, source(v));
super.set(key, v);
}
this.#size.v = sources.size;
@ -62,7 +80,8 @@ export class ReactiveMap extends Map {
forEach(callbackfn, this_arg) {
get(this.#version);
return super.forEach(callbackfn, this_arg);
var bound_callbackfn = callbackfn.bind(this_arg);
this.#sources.forEach((s, key) => bound_callbackfn(s.v, key, this));
}
/** @param {K} key */
@ -96,7 +115,7 @@ export class ReactiveMap extends Map {
set(s, value);
}
return super.set(key, value);
return this;
}
/** @param {K} key */
@ -105,13 +124,14 @@ export class ReactiveMap extends Map {
var s = sources.get(key);
if (s !== undefined) {
sources.delete(key);
var removed = sources.delete(key);
set(this.#size, sources.size);
set(s, /** @type {V} */ (UNINITIALIZED));
this.#increment_version();
return removed;
}
return super.delete(key);
return false;
}
clear() {
@ -126,7 +146,6 @@ export class ReactiveMap extends Map {
}
sources.clear();
super.clear();
}
keys() {

@ -124,6 +124,40 @@ test('map.has(...)', () => {
cleanup();
});
test('map.forEach(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
const log: any = [];
const this_arg = {};
map.forEach(function (this: unknown, ...args) {
log.push([...args, this]);
}, this_arg);
assert.deepEqual(log, [
[1, 1, map, this_arg],
[2, 2, map, this_arg],
[3, 3, map, this_arg]
]);
});
test('map.delete(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
assert.equal(map.delete(3), true);
assert.equal(map.delete(3), false);
assert.deepEqual(Array.from(map.values()), [1, 2]);
});
test('map handling of undefined values', () => {
const map = new ReactiveMap();

@ -2,6 +2,7 @@ import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { map } from './utils.js';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
@ -31,7 +32,6 @@ export class ReactiveSet extends Set {
for (var element of value) {
sources.set(element, source(true));
super.add(element);
}
this.#size.v = sources.size;
@ -66,6 +66,13 @@ export class ReactiveSet extends Set {
return new ReactiveSet(set);
};
}
if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#version);
};
}
}
#increment_version() {
@ -97,7 +104,7 @@ export class ReactiveSet extends Set {
this.#increment_version();
}
return super.add(value);
return this;
}
/** @param {T} value */
@ -106,13 +113,14 @@ export class ReactiveSet extends Set {
var s = sources.get(value);
if (s !== undefined) {
sources.delete(value);
var removed = sources.delete(value);
set(this.#size, sources.size);
set(s, false);
this.#increment_version();
return removed;
}
return super.delete(value);
return false;
}
clear() {
@ -127,7 +135,6 @@ export class ReactiveSet extends Set {
}
sources.clear();
super.clear();
}
keys() {

@ -77,3 +77,12 @@ test('set.has(...)', () => {
cleanup();
});
test('set.delete(...)', () => {
const set = new ReactiveSet([1, 2, 3]);
assert.equal(set.delete(3), true);
assert.equal(set.delete(3), false);
assert.deepEqual(Array.from(set.values()), [1, 2]);
});

@ -1,7 +1,11 @@
import { DEV } from 'esm-env';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
const REPLACE = Symbol();
var inited_url = false;
var inited_search_params = false;
export class ReactiveURL extends URL {
#protocol = source(super.protocol);
@ -21,6 +25,14 @@ export class ReactiveURL extends URL {
url = new URL(url, base);
super(url);
this.#searchParams[REPLACE](url.searchParams);
if (DEV && !inited_url) {
inited_url = true;
// @ts-ignore
ReactiveURL.prototype[INSPECT_SYMBOL] = function () {
this.href;
};
}
}
get hash() {
@ -159,6 +171,17 @@ export class ReactiveURLSearchParams extends URLSearchParams {
set(this.#version, this.#version.v + 1);
}
constructor() {
super();
if (DEV && !inited_search_params) {
inited_search_params = true;
// @ts-ignore
ReactiveURLSearchParams.prototype[INSPECT_SYMBOL] = function () {
get(this.#version);
};
}
}
/**
* @param {URLSearchParams} params
*/

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'conflicting-slot-usage',
message:
'Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.',
position: [71, 84]
}
});

@ -0,0 +1,6 @@
<script>
let { children } = $props();
</script>
{@render children()}
<slot></slot>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
test({ assert, target }) {
const path = target.querySelector('path');
assert.equal(path?.namespaceURI, 'http://www.w3.org/2000/svg');
}
});

@ -0,0 +1,11 @@
<svelte:options namespace="svg" />
<script>
import Svg from "./svg.svelte";
let tag = "path";
</script>
<Svg>
<svelte:element this="{tag}" d="M21 12a9 9 0 1 1-6.219-8.56"/>
</Svg>

@ -0,0 +1 @@
<svg><slot></slot></svg>

After

Width:  |  Height:  |  Size: 24 B

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.equal(target.querySelector('path')?.namespaceURI, 'http://www.w3.org/2000/svg');
await target.querySelector('button')?.click();
assert.equal(target.querySelector('div')?.namespaceURI, 'http://www.w3.org/1999/xhtml');
}
});

@ -0,0 +1,14 @@
<script>
let tag = $state('path');
let xmlns = $state('http://www.w3.org/2000/svg');
</script>
<button onclick={() => {
tag = 'div';
xmlns = null;
}}>change</button>
<!-- wrapper necessary or else jsdom says this is always an xhtml namespace -->
<svg>
<svelte:element this={tag} xmlns={xmlns} />
</svg>

@ -0,0 +1,39 @@
import { flushSync } from '../../../../src/index-client';
import { test } from '../../test';
export default test({
html: `<button>add item</button><button>make span</button><button>reverse</button>`,
async test({ assert, target }) {
const [btn1, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn1?.click();
btn1?.click();
btn1?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add item</button><button>make span</button><button>reverse</button><div>Item 1</div><div>Item 2</div><div>Item 3</div>`
);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add item</button><button>make span</button><button>reverse</button><span>Item 1</span><span>Item 2</span><span>Item 3</span>`
);
flushSync(() => {
btn3?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add item</button><button>make span</button><button>reverse</button><span>Item 3</span><span>Item 2</span><span>Item 1</span>`
);
}
});

@ -0,0 +1,30 @@
<script>
let items = $state([]);
function add_item() {
items.push({
id: items.length,
text: 'Item ' + (items.length + 1),
html: '<div>Item ' + (items.length + 1) + '</div>',
dom: null,
})
}
function make_span() {
items.forEach(item => {
item.html = item.html.replace(/div/g, 'span')
})
}
function reverse() {
items.reverse();
}
</script>
<button on:click={add_item}>add item</button>
<button on:click={make_span}>make span</button>
<button on:click={reverse}>reverse</button>
{#each items as item (item.id)}
{@html item.html}
{/each}

@ -0,0 +1,81 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
import { log } from './log';
export default test({
compileOptions: {
dev: true
},
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [in1, in2] = target.querySelectorAll('input');
const [b1, b2, b3] = target.querySelectorAll('button');
assert.deepEqual(log, [
{ label: 'map', type: 'init', values: [] },
{ label: 'set', type: 'init', values: [] },
{ label: 'date', type: 'init', values: 1712966400000 }
]);
log.length = 0;
flushSync(() => b1.click()); // map.set('key', 'value')
in1.value = 'name';
in2.value = 'Svelte';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b1.click()); // map.set('name', 'Svelte')
in2.value = 'World';
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b1.click()); // map.set('name', 'World')
flushSync(() => b1.click()); // map.set('name', 'World')
assert.deepEqual(log, [
{ label: 'map', type: 'update', values: [['key', 'value']] },
{
label: 'map',
type: 'update',
values: [
['key', 'value'],
['name', 'Svelte']
]
},
{
label: 'map',
type: 'update',
values: [
['key', 'value'],
['name', 'World']
]
}
]);
log.length = 0;
flushSync(() => b2.click()); // set.add('name');
in1.value = 'Svelte';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b2.click()); // set.add('Svelte');
flushSync(() => b2.click()); // set.add('Svelte');
assert.deepEqual(log, [
{ label: 'set', type: 'update', values: ['name'] },
{ label: 'set', type: 'update', values: ['name', 'Svelte'] }
]);
log.length = 0;
flushSync(() => b3.click()); // date.minutes++
flushSync(() => b3.click()); // date.minutes++
flushSync(() => b3.click()); // date.minutes++
assert.deepEqual(log, [
{ label: 'date', type: 'update', values: 1712966460000 },
{ label: 'date', type: 'update', values: 1712966520000 },
{ label: 'date', type: 'update', values: 1712966580000 }
]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,27 @@
<script>
import { Map, Set, Date } from 'svelte/reactivity';
import { log } from './log';
const map = new Map();
const set = new Set();
const date = new Date('2024-04-13 00:00:00+0000');
let key = $state('key');
let value = $state('value');
$inspect(map).with((type, map) => {
log.push({ label: 'map', type, values: [...map] });
});
$inspect(set).with((type, set) => {
log.push({ label: 'set', type, values: [...set] });
});
$inspect(date).with((type, date) => {
log.push({ label: 'date', type, values: date.getTime() });
});
</script>
<input bind:value={key} />
<input bind:value={value} />
<button on:click={() => map.set(key, value)}>map</button>
<button on:click={() => set.add(key)}>set</button>
<button on:click={() => date.setMinutes(date.getMinutes() + 1)}>date</button>

@ -0,0 +1,10 @@
<script>
let { item } = $props();
function onclick() {
item.name = `${item.name} edited`
}
</script>
<div>{item?.name}</div>
<button {onclick}>Then click here</button>

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
/** @type {typeof console.warn} */
let warn;
/** @type {any[]} */
let warnings = [];
export default test({
compileOptions: {
dev: true
},
before_test: () => {
warn = console.warn;
console.warn = (...args) => {
warnings.push(...args);
};
},
after_test: () => {
console.warn = warn;
warnings = [];
},
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
btn1.click();
await tick();
assert.deepEqual(warnings.length, 0);
btn2.click();
await tick();
assert.deepEqual(warnings.length, 1);
}
});

@ -0,0 +1,13 @@
<script>
import Child from './Child.svelte';
let items = $state([{ id: "test", name: "this is a test"}, { id:"test2", name: "this is a second test"}]);
let found = $state();
function onclick() {
found = items.find(c => c.id === 'test2');
}
</script>
<button {onclick}>First click here</button>
<Child item={found} />

@ -0,0 +1,14 @@
[
{
"code": "invalid-css-global-selector-list",
"message": ":global(...) must not contain type or universal selectors when used in a compound selector",
"start": {
"line": 20,
"column": 6
},
"end": {
"line": 20,
"column": 17
}
}
]

@ -0,0 +1,27 @@
<style>
::foo:global([data-state='checked']) {
color: red;
}
::foo:global(.foo) {
color: red;
}
::foo:global(#foo) {
color: red;
}
::foo:global(::foo) {
color: red;
}
::foo:global(:foo) {
color: red;
}
:global(h1) {
color: red;
}
::foo:global(h1) {
color: red;
}
</style>
<div>
<h1>hello world</h1>
</div>

@ -0,0 +1,14 @@
[
{
"code": "invalid-css-type-selector-placement",
"message": ":global(...) must not be followed with a type selector",
"start": {
"line": 17,
"column": 14
},
"end": {
"line": 17,
"column": 16
}
}
]

@ -0,0 +1,24 @@
<style>
:global(.foo):foo {
color: red;
}
:global(.foo)::foo {
color: red;
}
:global(.foo).bar {
color: red;
}
:global(.foo)#baz {
color: red;
}
:global(.foo)[id] {
color: red;
}
:global(.foo)h1 {
color: red;
}
</style>
<div>
<h1 class="bar" id="baz">hello world</h1>
</div>

@ -0,0 +1,16 @@
<script>
function test() {
try {
throw new TypeError("oops1");
} catch (error) {
console.log(error);
}
try {
throw new TypeError("oops2");
} catch (error) {
console.log(error);
}
}
test();
</script>

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

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

@ -0,0 +1,14 @@
[
{
"code": "invalid-bindable-declaration",
"message": "Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?",
"start": {
"column": 7,
"line": 2
},
"end": {
"column": 20,
"line": 2
}
}
]

@ -0,0 +1,13 @@
<script>
let { foo } = $props();
</script>
<!-- ok -->
<button onclick={foo}>click me</button>
<Button onclick={foo}>click me</Button>
<Button on:click={foo}>click me</Button>
<!-- warn -->
<slot></slot>
<slot name="foo"></slot>
<button on:click={foo}>click me</button>

@ -0,0 +1,38 @@
[
{
"code": "deprecated-slot-element",
"end": {
"column": 13,
"line": 11
},
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
"start": {
"column": 0,
"line": 11
}
},
{
"code": "deprecated-slot-element",
"end": {
"column": 24,
"line": 12
},
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
"start": {
"column": 0,
"line": 12
}
},
{
"code": "deprecated-event-handler",
"end": {
"column": 22,
"line": 13
},
"message": "Using on:click to listen to the click event is is deprecated. Use the event attribute onclick instead.",
"start": {
"column": 8,
"line": 13
}
}
]

@ -8,6 +8,6 @@
console.log(doubled);
</script>
<button on:click={() => count += 1}>
<button onclick={() => count += 1}>
clicks: {count}
</button>

@ -1577,10 +1577,10 @@ declare module 'svelte/compiler' {
tag: Expression;
metadata: {
/**
* `true`/`false` if this is definitely (not) an svg element.
* `null` means we can't know statically.
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.
*/
svg: boolean | null;
svg: boolean;
scoped: boolean;
};
}
@ -2059,6 +2059,7 @@ declare module 'svelte/reactivity' {
#private;
}
class ReactiveURLSearchParams extends URLSearchParams {
constructor();
[REPLACE](params: URLSearchParams): void;
#private;

Loading…
Cancel
Save