Merge branch 'main' into simplify-transitions

pull/10798/head
Rich Harris 2 years ago
commit 8863459420

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add support for webkitdirectory DOM boolean attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't override instance methods during legacy class creation

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Map class to svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: adjust scope parent for named slots

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handling of unowned derived signals

@ -13,6 +13,7 @@
"afraid-moose-matter",
"angry-books-jam",
"angry-plums-punch",
"beige-cobras-smoke",
"beige-flies-wash",
"beige-mirrors-listen",
"beige-rabbits-shave",
@ -63,6 +64,7 @@
"dry-eggs-retire",
"dull-coins-vanish",
"dull-mangos-wave",
"dull-pots-add",
"dull-roses-relate",
"early-ads-tie",
"eight-steaks-shout",
@ -98,6 +100,7 @@
"friendly-candles-relate",
"friendly-lies-camp",
"funny-wombats-argue",
"fuzzy-bags-camp",
"gentle-dolls-juggle",
"gentle-sheep-hug",
"gentle-spies-happen",
@ -165,6 +168,7 @@
"lucky-schools-hang",
"lucky-toes-begin",
"many-trees-fix",
"mighty-cooks-scream",
"mighty-files-hammer",
"moody-carrots-lay",
"moody-frogs-exist",
@ -191,6 +195,7 @@
"old-mails-sneeze",
"old-oranges-compete",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
@ -232,8 +237,10 @@
"rotten-rules-invite",
"rude-ghosts-tickle",
"selfish-dragons-knock",
"selfish-spies-help",
"selfish-tools-hide",
"serious-kids-deliver",
"serious-needles-joke",
"serious-socks-cover",
"serious-zebras-scream",
"seven-deers-jam",
@ -263,6 +270,7 @@
"small-papayas-laugh",
"small-sheep-type",
"smart-parents-swim",
"smart-turkeys-tell",
"smart-zebras-pay",
"smooth-rings-rush",
"soft-clocks-remember",
@ -304,6 +312,7 @@
"tasty-cheetahs-appear",
"tasty-numbers-perform",
"tasty-steaks-smile",
"ten-eels-move",
"ten-foxes-repeat",
"ten-jokes-divide",
"ten-peaches-sleep",
@ -344,6 +353,7 @@
"wild-foxes-wonder",
"wise-apples-care",
"wise-dancers-hang",
"wise-dodos-tell",
"wise-donkeys-marry",
"wise-jobs-admire",
"wise-radios-exercise",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve element class attribute behaviour

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add types for svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure select value is updated upon select option removal

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure capture events don't call delegated events

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure arguments are supported on all reactive Date methods

@ -60,3 +60,18 @@ jobs:
- name: build and check generated types
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }
Treeshaking:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile
- name: check
run: cd packages/svelte && node scripts/check-treeshakeability.js

@ -1,5 +1,37 @@
# svelte
## 5.0.0-next.81
### Patch Changes
- feat: add support for webkitdirectory DOM boolean attribute ([#10847](https://github.com/sveltejs/svelte/pull/10847))
- fix: don't override instance methods during legacy class creation ([#10834](https://github.com/sveltejs/svelte/pull/10834))
- fix: adjust scope parent for named slots ([#10843](https://github.com/sveltejs/svelte/pull/10843))
- fix: improve handling of unowned derived signals ([#10842](https://github.com/sveltejs/svelte/pull/10842))
- fix: improve element class attribute behaviour ([#10856](https://github.com/sveltejs/svelte/pull/10856))
- fix: ensure select value is updated upon select option removal ([#10846](https://github.com/sveltejs/svelte/pull/10846))
- fix: ensure capture events don't call delegated events ([#10831](https://github.com/sveltejs/svelte/pull/10831))
## 5.0.0-next.80
### Patch Changes
- fix: add types for svelte/reactivity ([#10817](https://github.com/sveltejs/svelte/pull/10817))
- fix: ensure arguments are supported on all reactive Date methods ([#10813](https://github.com/sveltejs/svelte/pull/10813))
## 5.0.0-next.79
### Patch Changes
- feat: add reactive Map class to svelte/reactivity ([#10803](https://github.com/sveltejs/svelte/pull/10803))
## 5.0.0-next.78
### Patch Changes

@ -1056,6 +1056,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
type?: HTMLInputTypeAttribute | undefined | null;
value?: any;
width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
'on:change'?: ChangeEventHandler<HTMLInputElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLInputElement> | undefined | null;

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

@ -29,6 +29,7 @@ await createBundle({
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -162,26 +162,6 @@ function serialize_class_directives(class_directives, element_id, context, is_at
}
}
/**
*
* @param {string | null} spread_id
* @param {import('#compiler').RegularElement} node
* @param {import('../types.js').ComponentContext} context
* @param {import('estree').Identifier} node_id
*/
function add_select_to_spread_update(spread_id, node, context, node_id) {
if (spread_id !== null && node.name === 'select') {
context.state.update.push({
grouped: b.if(
b.binary('in', b.literal('value'), b.id(spread_id)),
b.block([
b.stmt(b.call('$.select_option', node_id, b.member(b.id(spread_id), b.id('value'))))
])
)
});
}
}
/**
* @param {import('#compiler').Binding[]} references
* @param {import('../types.js').ComponentContext} context
@ -227,6 +207,8 @@ function collect_transitive_dependencies(binding, seen = new Set()) {
* @param {import('../types.js').ComponentContext} context
*/
function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression;
while (bound.type === 'MemberExpression') {
bound = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
@ -247,59 +229,49 @@ function setup_select_synchronization(value_binding, context) {
}
}
if (!context.state.analysis.runes) {
const invalidator = b.call(
'$.invalidate_inner_signals',
b.thunk(
b.block(
names.map((name) => {
const serialized = serialize_get_binding(b.id(name), context.state);
return b.stmt(serialized);
})
)
const invalidator = b.call(
'$.invalidate_inner_signals',
b.thunk(
b.block(
names.map((name) => {
const serialized = serialize_get_binding(b.id(name), context.state);
return b.stmt(serialized);
})
)
);
)
);
context.state.init.push(
b.stmt(
b.call(
'$.invalidate_effect',
b.thunk(
b.block([
b.stmt(
/** @type {import('estree').Expression} */ (context.visit(value_binding.expression))
),
b.stmt(invalidator)
])
)
context.state.init.push(
b.stmt(
b.call(
'$.invalidate_effect',
b.thunk(
b.block([
b.stmt(
/** @type {import('estree').Expression} */ (context.visit(value_binding.expression))
),
b.stmt(invalidator)
])
)
)
);
}
)
);
}
/**
* Serializes element attribute assignments that contain spreads to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* $.spread_attributes(element, null, [...]);
* ```
* Resulting code for dynamic looks something like this:
* ```js
* let value;
* $.render_effect(() => {
* value = $.spread_attributes(element, value, [...])
* });
* ```
* Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise.
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context
* @param {import('#compiler').RegularElement} element
* @param {import('estree').Identifier} element_id
* @returns {string | null}
* @param {boolean} needs_select_handling
*/
function serialize_element_spread_attributes(attributes, context, element, element_id) {
function serialize_element_spread_attributes(
attributes,
context,
element,
element_id,
needs_select_handling
) {
let needs_isolation = false;
/** @type {import('estree').Expression[]} */
@ -321,8 +293,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
const lowercase_attributes =
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
const id = context.state.scope.generate('spread_attributes');
const isolated = b.stmt(
const standalone = b.stmt(
b.call(
'$.spread_attributes_effect',
element_id,
@ -331,32 +304,57 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
b.literal(context.state.analysis.css.hash)
)
);
const inside_effect = b.stmt(
b.assignment(
'=',
b.id(id),
b.call(
'$.spread_attributes',
element_id,
b.id(id),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.css.hash)
)
)
);
if (!needs_isolation || needs_select_handling) {
context.state.init.push(b.let(id));
}
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
context.state.update_effects.push(isolated);
return null;
if (needs_select_handling) {
context.state.update_effects.push(
b.stmt(b.call('$.render_effect', b.arrow([], b.block([inside_effect]))))
);
} else {
context.state.update_effects.push(standalone);
}
} else {
const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id));
context.state.update.push({
singular: isolated,
grouped: b.stmt(
b.assignment(
'=',
b.id(id),
b.call(
'$.spread_attributes',
element_id,
b.id(id),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.css.hash)
)
)
singular: needs_select_handling ? undefined : standalone,
grouped: inside_effect
});
}
if (needs_select_handling) {
context.state.init.push(
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(b.id(id), b.id('value')))))
);
context.state.update.push({
grouped: b.if(
b.binary('in', b.literal('value'), b.id(id)),
b.block([
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.stmt(b.call('$.select_option', element_id, b.member(b.id(id), b.id('value'))))
])
)
});
return id;
}
}
@ -479,6 +477,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
function serialize_element_attribute_update_assignment(element, node_id, attribute, context) {
const state = context.state;
const name = get_attribute_name(element, attribute, context);
const is_svg = context.state.metadata.namespace === 'svg';
let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context);
// The foreign namespace doesn't have any special handling, everything goes through the attr function
@ -513,13 +512,19 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
if (name === 'class') {
if (singular) {
return {
singular: b.stmt(b.call('$.class_name_effect', node_id, b.thunk(singular))),
grouped: b.stmt(b.call('$.class_name', node_id, singular)),
singular: b.stmt(
b.call(
is_svg ? '$.svg_class_name_effect' : '$.class_name_effect',
node_id,
b.thunk(singular)
)
),
grouped: b.stmt(b.call(is_svg ? '$.svg_class_name' : '$.class_name', node_id, singular)),
skip_condition: true
};
}
return {
grouped: b.stmt(b.call('$.class_name', node_id, value)),
grouped: b.stmt(b.call(is_svg ? '$.svg_class_name' : '$.class_name', node_id, value)),
skip_condition: true
};
} else if (!DOMProperties.includes(name)) {
@ -648,27 +653,27 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
)
);
const is_reactive = attribute.metadata.dynamic;
const needs_selected_call =
element === 'option' && (is_reactive || collect_parent_each_blocks(context).length > 0);
const needs_option_call = element === 'select' && is_reactive;
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const assignment = b.stmt(
needs_selected_call
is_select_with_value
? b.sequence([
inner_assignment,
// This ensures things stay in sync with the select binding
// in case of updates to the option value or new values appearing
b.call('$.selected', node_id)
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: needs_option_call
? b.sequence([
inner_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>
b.call('$.select_option', node_id, value)
])
: inner_assignment
: inner_assignment
);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (is_reactive) {
const id = state.scope.generate(`${node_id.name}_value`);
serialize_update_assignment(
@ -2082,11 +2087,15 @@ export const template_visitors = {
// Then do attributes
let is_attributes_reactive = false;
if (node.metadata.has_spread) {
const spread_id = serialize_element_spread_attributes(attributes, context, node, node_id);
if (child_metadata.namespace !== 'foreign') {
add_select_to_spread_update(spread_id, node, context, node_id);
}
is_attributes_reactive = spread_id !== null;
serialize_element_spread_attributes(
attributes,
context,
node,
node_id,
// If value binding exists, that one takes care of calling $.init_select
value_binding === null && node.name === 'select' && child_metadata.namespace !== 'foreign'
);
is_attributes_reactive = true;
} else {
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {

@ -400,9 +400,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
) {
// <div slot="..."> inherits the scope above the component, because slots are hella weird
scopes.set(child, state.scope);
visit(child);
// <div slot="..."> inherits the scope above the component unless the component is a named slot itself, because slots are hella weird
scopes.set(child, is_default_slot ? state.scope : scope);
visit(child, { scope: is_default_slot ? state.scope : scope });
} else {
if (child.type === 'ExpressionTag') {
// expression tag is a special case — we don't visit it directly, but via process_children,
@ -599,26 +599,38 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
AwaitBlock(node, context) {
context.next();
context.visit(node.expression);
if (node.pending) {
context.visit(node.pending);
}
if (node.then && node.value !== null) {
const then_scope = /** @type {Scope} */ (scopes.get(node.then));
const value_scope = context.state.scope.child();
for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'normal', 'const');
value_scope.declare(id, 'normal', 'const');
if (node.then) {
context.visit(node.then);
if (node.value) {
const then_scope = /** @type {Scope} */ (scopes.get(node.then));
const value_scope = context.state.scope.child();
scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'normal', 'const');
value_scope.declare(id, 'normal', 'const');
}
}
scopes.set(node.value, value_scope);
}
if (node.catch && node.error !== null) {
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
const error_scope = context.state.scope.child();
for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'normal', 'const');
error_scope.declare(id, 'normal', 'const');
if (node.catch) {
context.visit(node.catch);
if (node.error) {
const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
const error_scope = context.state.scope.child();
scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'normal', 'const');
error_scope.declare(id, 'normal', 'const');
}
}
scopes.set(node.error, error_scope);
}
},

@ -87,7 +87,8 @@ export const DOMBooleanAttributes = [
'required',
'reversed',
'seamless',
'selected'
'selected',
'webkitdirectory'
];
export const namespace_svg = 'http://www.w3.org/2000/svg';

@ -1,4 +1,5 @@
import { effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
@ -26,26 +27,43 @@ export function select_option(select, value, mounting) {
}
/**
* Finds the containing `<select>` element and potentially updates its `selected` state.
* @param {HTMLOptionElement} option
* @returns {void}
* Selects the correct option(s) if `value` is given,
* and then sets up a mutation observer to sync the
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* @template V
* @param {HTMLSelectElement} select
* @param {() => V} [get_value]
*/
export function selected(option) {
// Inside an effect because the element might not be connected
// to the parent <select> yet when this is called
export function init_select(select, get_value) {
effect(() => {
var select = option.parentNode;
while (select != null) {
if (select.nodeName === 'SELECT') break;
select = select.parentNode;
if (get_value) {
select_option(select, untrack(get_value));
}
// @ts-ignore
if (select != null && option.__value === select.__value) {
// never set to false, since this causes browser to select default option
option.selected = true;
}
var observer = new MutationObserver(() => {
// @ts-ignore
var value = select.__value;
select_option(select, value);
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
observer.observe(select, {
// Listen to option element changes
childList: true,
subtree: true, // because of <optgroup>
// Listen to option element value attribute changes
// (doesn't get notified of select value changes,
// because that property is not reflected as an attribute)
attributes: true,
attributeFilter: ['value']
});
return () => {
observer.disconnect();
};
});
}
@ -78,6 +96,7 @@ export function bind_select_value(select, get_value, update) {
var value = get_value();
select_option(select, value, mounting);
// Mounting and value undefined -> take selection from dom
if (mounting && value === undefined) {
/** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked');
@ -91,6 +110,9 @@ export function bind_select_value(select, get_value, update) {
select.__value = value;
mounting = false;
});
// don't pass get_value, we already initialize it in the effect above
init_select(select);
}
/**

@ -3,7 +3,7 @@ import { set_class_name } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
/**
* @param {Element} dom
* @param {HTMLElement} dom
* @param {() => string} value
* @returns {void}
*/
@ -14,7 +14,47 @@ export function class_name_effect(dom, value) {
}
/**
* @param {Element} dom
* @param {SVGElement} dom
* @param {() => string} value
* @returns {void}
*/
export function svg_class_name_effect(dom, value) {
render_effect(() => {
svg_class_name(dom, value());
});
}
/**
* @param {SVGElement} dom
* @param {string} value
* @returns {void}
*/
export function svg_class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {HTMLElement} dom
* @param {string} value
* @returns {void}
*/
@ -31,7 +71,10 @@ export function class_name(dom, value) {
prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name)
) {
if (next_class_name === '') {
// Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null) {
dom.removeAttribute('class');
} else {
set_class_name(dom, next_class_name);

@ -17,7 +17,10 @@ export function event(event_name, dom, handler, capture, passive) {
* @this {EventTarget}
*/
function target_handler(/** @type {Event} */ event) {
handle_event_propagation(dom, event);
if (!capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation(dom, event);
}
if (!event.cancelBubble) {
return handler.call(this, event);
}

@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) {
const prototype = get_prototype_of(value);
// TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, state_proxy_handler);

@ -41,7 +41,7 @@ export function derived(fn) {
/** @type {import('#client').DerivedDebug} */ (signal).inspect = new Set();
}
if (current_reaction !== null) {
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
if (current_reaction.deriveds === null) {
current_reaction.deriveds = [signal];
} else {

@ -11,13 +11,13 @@ import * as $ from '../internal/index.js';
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
* @param {import('svelte').ComponentConstructorOptions<Props> & {
* component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: boolean;
* }} options
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
* @returns {import('svelte').SvelteComponent<Props, Events, Slots> & Exports}
*/
export function createClassComponent(options) {
// @ts-expect-error $$prop_def etc are not actually defined
@ -34,8 +34,8 @@ export function createClassComponent(options) {
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('../main/public.js').SvelteComponent<Props, Events, Slots>} component
* @returns {import('../main/public.js').ComponentType<import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports>}
* @param {import('svelte').SvelteComponent<Props, Events, Slots>} component
* @returns {import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots> & Exports>}
*/
export function asClassComponent(component) {
// @ts-expect-error $$prop_def etc are not actually defined
@ -58,7 +58,7 @@ class Svelte4Component {
#instance;
/**
* @param {import('../main/public.js').ComponentConstructorOptions & {
* @param {import('svelte').ComponentConstructorOptions & {
* component: any;
* immutable?: boolean;
* hydrate?: boolean;
@ -79,6 +79,7 @@ class Svelte4Component {
});
for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy' || key === '$on') continue;
define_property(this, key, {
get() {
return this.#instance[key];

@ -55,29 +55,30 @@ const write = [
'setYear'
];
var inited = false;
export class ReactiveDate extends Date {
#raw_time = source(super.getTime());
static #inited = false;
// We init as part of the first instance so that we can treeshake this class
#init() {
if (!ReactiveDate.#inited) {
ReactiveDate.#inited = true;
if (!inited) {
inited = true;
const proto = ReactiveDate.prototype;
const date_proto = Date.prototype;
for (const method of read) {
// @ts-ignore
proto[method] = function () {
proto[method] = function (...args) {
get(this.#raw_time);
// @ts-ignore
return date_proto[method].call(this);
return date_proto[method].apply(this, args);
};
}
for (const method of write) {
// @ts-ignore
proto[method] = function (/** @type {any} */ ...args) {
proto[method] = function (...args) {
// @ts-ignore
const v = date_proto[method].apply(this, args);
const time = date_proto.getTime.call(this);

@ -1,2 +1,3 @@
export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';

@ -0,0 +1,157 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../internal/client/constants.js';
import { map } from './utils.js';
/**
* @template K
* @template V
*/
export class ReactiveMap extends Map {
/** @type {Map<K, import('#client').Source<V>>} */
#sources = new Map();
#version = source(0);
#size = source(0);
/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
*/
constructor(value) {
super();
// If the value is invalid then the native exception will fire here
if (DEV) new Map(value);
if (value) {
var sources = this.#sources;
for (var [key, v] of value) {
sources.set(key, source(v));
super.set(key, v);
}
this.#size.v = sources.size;
}
}
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {K} key */
has(key) {
var s = this.#sources.get(key);
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return false;
}
get(s);
return true;
}
/**
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
* @param {any} [this_arg]
*/
forEach(callbackfn, this_arg) {
get(this.#version);
return super.forEach(callbackfn, this_arg);
}
/** @param {K} key */
get(key) {
var s = this.#sources.get(key);
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return undefined;
}
return get(s);
}
/**
* @param {K} key
* @param {V} value
* */
set(key, value) {
var sources = this.#sources;
var s = sources.get(key);
if (s === undefined) {
sources.set(key, source(value));
set(this.#size, sources.size);
this.#increment_version();
} else {
set(s, value);
}
return super.set(key, value);
}
/** @param {K} key */
delete(key) {
var sources = this.#sources;
var s = sources.get(key);
if (s !== undefined) {
sources.delete(key);
set(this.#size, sources.size);
set(s, /** @type {V} */ (UNINITIALIZED));
this.#increment_version();
}
return super.delete(key);
}
clear() {
var sources = this.#sources;
if (sources.size !== 0) {
set(this.#size, 0);
for (var s of sources.values()) {
set(s, /** @type {V} */ (UNINITIALIZED));
}
this.#increment_version();
}
sources.clear();
super.clear();
}
keys() {
get(this.#version);
return this.#sources.keys();
}
values() {
get(this.#version);
return map(this.#sources.values(), get);
}
entries() {
get(this.#version);
return map(
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
);
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
return get(this.#size);
}
}

@ -0,0 +1,151 @@
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../main/main-client.js';
import { ReactiveMap } from './map.js';
import { assert, test } from 'vitest';
test('map.values()', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push(map.size);
});
pre_effect(() => {
log.push(map.has(3));
});
pre_effect(() => {
log.push(Array.from(map.values()));
});
});
flushSync(() => {
map.delete(3);
});
flushSync(() => {
map.clear();
});
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug
cleanup();
});
test('map.get(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('get 1', map.get(1));
});
pre_effect(() => {
log.push('get 2', map.get(2));
});
pre_effect(() => {
log.push('get 3', map.get(3));
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);
cleanup();
});
test('map.has(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('has 1', map.has(1));
});
pre_effect(() => {
log.push('has 2', map.has(2));
});
pre_effect(() => {
log.push('has 3', map.has(3));
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, [
'has 1',
true,
'has 2',
true,
'has 3',
true,
'has 2',
false,
'has 2',
true
]);
cleanup();
});
test('map handling of undefined values', () => {
const map = new ReactiveMap();
const log: any = [];
const cleanup = user_root_effect(() => {
map.set(1, undefined);
pre_effect(() => {
log.push(map.get(1));
});
flushSync(() => {
map.delete(1);
});
flushSync(() => {
map.set(1, 1);
});
});
assert.deepEqual(log, [undefined, undefined, 1]);
cleanup();
});

@ -1,33 +1,10 @@
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';
var read = [
'difference',
'forEach',
'intersection',
'isDisjointFrom',
'isSubsetOf',
'isSupersetOf',
'symmetricDifference',
'union'
];
/**
* @template T
* @param {IterableIterator<T>} iterator
*/
function make_iterable(iterator) {
iterator[Symbol.iterator] = get_self;
return iterator;
}
/**
* @this {any}
*/
function get_self() {
return this;
}
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
var inited = false;
@ -41,7 +18,7 @@ export class ReactiveSet extends Set {
#size = source(0);
/**
* @param {Iterable<T> | null | undefined} value
* @param {Iterable<T> | null | undefined} [value]
*/
constructor(value) {
super();
@ -50,9 +27,14 @@ export class ReactiveSet extends Set {
if (DEV) new Set(value);
if (value) {
var sources = this.#sources;
for (var element of value) {
this.add(element);
sources.set(element, source(true));
super.add(element);
}
this.#size.v = sources.size;
}
if (!inited) this.#init();
@ -65,7 +47,10 @@ export class ReactiveSet extends Set {
var proto = ReactiveSet.prototype;
var set_proto = Set.prototype;
for (var method of read) {
/** @type {string} */
var method;
for (method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
@ -73,6 +58,17 @@ export class ReactiveSet extends Set {
return set_proto[method].apply(this, v);
};
}
for (method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new ReactiveSet(set);
};
}
}
#increment_version() {
@ -81,9 +77,9 @@ export class ReactiveSet extends Set {
/** @param {T} value */
has(value) {
var source = this.#sources.get(value);
var s = this.#sources.get(value);
if (source === undefined) {
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
@ -91,7 +87,7 @@ export class ReactiveSet extends Set {
return false;
}
return get(source);
return get(s);
}
/** @param {T} value */
@ -110,12 +106,12 @@ export class ReactiveSet extends Set {
/** @param {T} value */
delete(value) {
var sources = this.#sources;
var source = sources.get(value);
var s = sources.get(value);
if (source !== undefined) {
if (s !== undefined) {
sources.delete(value);
set(this.#size, sources.size);
set(source, false);
set(s, false);
this.#increment_version();
}
@ -127,8 +123,8 @@ export class ReactiveSet extends Set {
if (sources.size !== 0) {
set(this.#size, 0);
for (var source of sources.values()) {
set(source, false);
for (var s of sources.values()) {
set(s, false);
}
this.#increment_version();
}
@ -138,45 +134,20 @@ export class ReactiveSet extends Set {
}
keys() {
return this.values();
get(this.#version);
return this.#sources.keys();
}
values() {
get(this.#version);
var iterator = this.#sources.keys();
return make_iterable(
/** @type {IterableIterator<T>} */ ({
next() {
for (var value of iterator) {
return { value, done: false };
}
return { done: true };
}
})
);
return this.keys();
}
entries() {
var iterator = this.values();
return make_iterable(
/** @type {IterableIterator<[T, T]>} */ ({
next() {
for (var value of iterator) {
return { value: [value, value], done: false };
}
return { done: true };
}
})
);
return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]));
}
[Symbol.iterator]() {
return this.values();
return this.keys();
}
get size() {

@ -0,0 +1,24 @@
/**
* @template T
* @template U
* @param {Iterable<T>} iterable
* @param {(value: T) => U} fn
* @returns {IterableIterator<U>}
*/
export function map(iterable, fn) {
return {
[Symbol.iterator]: get_this,
next() {
for (const value of iterable) {
return { done: false, value: fn(value) };
}
return { done: true, value: undefined };
}
};
}
/** @this {any} */
function get_this() {
return this;
}

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

@ -1 +1 @@
<!--ssr:0--><div class="foo"></div><!--ssr:0-->
<!--ssr:0--><div id="foo"></div><!--ssr:0-->

@ -1,5 +1,5 @@
<script>
export let className;
export let id;
</script>
<div class={className}></div>
<div id={id}></div>

@ -14,9 +14,11 @@ export default test({
<p>selected: two</p>
`,
test({ assert, component, target }) {
async test({ assert, component, target }) {
component.items = ['one', 'two', 'three'];
await Promise.resolve(); // mutation observer
const options = target.querySelectorAll('option');
assert.ok(!options[0].selected);
assert.ok(options[1].selected);

@ -19,9 +19,11 @@ export default test({
<p>selected: two</p>
`,
test({ assert, component, target }) {
async test({ assert, component, target }) {
component.items = ['one', 'two', 'three'];
await Promise.resolve(); // mutation observer
const options = target.querySelectorAll('option');
assert.ok(!options[0].selected);
assert.ok(options[1].selected);

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await new Promise((r) => setTimeout(r, 200)); // wait for await block to resolve
const options = target.querySelectorAll('option');
assert.ok(!options[0].selected);
assert.ok(options[1].selected);
assert.ok(!options[2].selected);
}
});

@ -0,0 +1,19 @@
<script>
let promise = getNumbers();
let selected = 2;
async function getNumbers() {
await new Promise(resolve => setTimeout(resolve, 100));
return [1, 2, 3];
}
</script>
<select bind:value={selected}>
{#await promise}
<option>-1</option>
{:then numbers}
{#each numbers as number}
<option>{number}</option>
{/each}
{/await}
</select>

@ -0,0 +1,35 @@
import { ok, test } from '../../test';
// test select binding behavior when a selected option is removed
export default test({
skip_if_ssr: 'permanent',
html: `<p>selected: a</p><select><option value="a">a</option><option value="b">b</option><option value="c">c</option></select>`,
async test({ assert, component, target }) {
const select = target.querySelector('select');
ok(select);
const options = target.querySelectorAll('option');
// first option should be selected by default since no value was bound
assert.equal(component.selected, 'a');
assert.equal(select.value, 'a');
assert.ok(options[0].selected);
// remove the selected item, so the bound value no longer matches anything
component.items = ['b', 'c'];
// There's a MutationObserver
await Promise.resolve();
// now no option should be selected
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
// model of selected value should be kept around, even if it is not in the list
assert.htmlEqual(
target.innerHTML,
`<p>selected: a</p><select><option value="b">b</option><option value="c">c</option></select>`
);
}
});

@ -0,0 +1,12 @@
<script>
export let selected;
export let items = ['a', 'b', 'c'];
</script>
<p>selected: {selected}</p>
<select bind:value={selected}>
{#each items as letter}
<option>{letter}</option>
{/each}
</select>

@ -13,7 +13,7 @@
<span
on:click="{toggle}"
class="{isCurrentlySelected ? 'selected' : ''}"
class="{isCurrentlySelected ? 'selected' : null}"
>
<slot></slot>
</span>
</span>

@ -0,0 +1,9 @@
<script>
export let text;
</script>
<div>
{text}
<hr />
<slot name="footer" />
</div>

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
html: `
<div>hello world <hr> <div slot="footer">hello world</div></div>
`
});

@ -0,0 +1,12 @@
<script>
import Nested from "./Nested.svelte"
import Nested2 from "./Nested2.svelte"
</script>
<Nested>
<Nested2 slot="inner" let:text {text}>
<div slot="footer">
{text}
</div>
</Nested2>
</Nested>

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await target.querySelector('button')?.click();
await Promise.resolve();
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
assert.equal(options[2].selected, false);
assert.equal(options[3].selected, false);
assert.equal(options[4].selected, true);
assert.equal(options[5].selected, false);
}
});

@ -0,0 +1,33 @@
<script>
let value = 'bar';
let value_bound = 'bar';
let options = {};
function loadOptions() {
options = {
foo: 'Foo',
bar: 'Bar',
baz: 'Baz',
};
}
</script>
<select {value}>
{#each Object.entries(options) as [key, value] (key)}
<option value={key}>
{value}
</option>
{/each}
</select>
<select bind:value={value_bound}>
{#each Object.entries(options) as [key, value] (key)}
<option value={key}>
{value}
</option>
{/each}
</select>
<button on:click={loadOptions}>
Load options
</button>

@ -0,0 +1,21 @@
import { test } from '../../test';
const items = [{ id: 'a' }, { id: 'b' }];
export default test({
get props() {
return { foo: [items[0]], items };
},
test({ assert, component, target }) {
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
component.foo = [component.items[1]];
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
}
});

@ -0,0 +1,9 @@
<script>
export let foo;
export let items;
</script>
<select value="{foo}" multiple>
<option value='{items[0]}'>a</option>
<option value='{items[1]}'>b</option>
</select>

@ -0,0 +1,30 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
let [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Activate</button><button>Toggle</button>\nneighba\nneighba`
);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Activate</button><button>Toggle</button>\nzeeba\nzeeba`
);
}
});

@ -0,0 +1,49 @@
<script>
import { untrack } from 'svelte';
class Model {
data = $state();
constructor(data) {
this.data = data;
}
name = $derived(this.data?.name);
source = $derived(this.data?.source);
toggle() {
this.data.name = this.data.name === 'zeeba' ? 'neighba' : 'zeeba';
}
}
let model = $state(new Model({ name: 'zeeba', source: 'initial' }));
let setModel = (source) => {
let next = new Model({ name: 'zeeba', source });
model = next;
}
let needsSet = $state(false);
$effect(() => {
if(needsSet) {
setModel('effect');
untrack(() => {
needsSet = false;
});
}
});
let setWithEffect = () => {
needsSet = true;
};
let toggle = () => {
model.toggle();
}
</script>
<button onclick={setWithEffect}>Activate</button>
<button onclick={toggle}>Toggle</button>
{model.name}
{model.data.name}

@ -0,0 +1,16 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
await Promise.resolve();
assert.deepEqual(log, ['div onclickcapture', 'button onclick']);
}
});

@ -0,0 +1,7 @@
<script>
import { log } from "./log";
</script>
<div onclickcapture={() => log.push('div onclickcapture')}>
<button onclick={() => log.push('button onclick')}>main</button>
</div>

@ -0,0 +1,50 @@
import { flushSync } from '../../../../src/main/main-client';
import { test } from '../../test';
export default test({
html: `<button>add</button><button>delete</button><button>clear</button>`,
test({ assert, target }) {
const [btn, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div>`
);
flushSync(() => {
btn?.click();
});
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div><div>3:3</div>`
);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div>`
);
flushSync(() => {
btn3?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button>`
);
}
});

@ -0,0 +1,21 @@
<script>
import {Map} from 'svelte/reactivity';
let state = new Map();
</script>
<button onclick={() => {
state.set(state.size + 1, state.size + 1);
}}>add</button>
<button onclick={() => {
state.delete(state.size);
}}>delete</button>
<button onclick={() => {
state.clear();
}}>clear</button>
{#each state as [key, value]}
<div>{key}:{value}</div>
{/each}

@ -170,5 +170,5 @@ asLegacyComponent.anExport;
const x: typeof asLegacyComponent = createClassComponent({
target: null as any,
hydrate: true,
component: newComponent
component: NewComponent
});

@ -1728,184 +1728,19 @@ declare module 'svelte/legacy' {
* @deprecated Use this only as a temporary solution to migrate your imperative component code to Svelte 5.
*
* */
export function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: ComponentConstructorOptions<Props> & {
component: SvelteComponent<Props, Events, Slots>;
export function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: import("svelte").ComponentConstructorOptions<Props> & {
component: import("svelte").ComponentType<import("svelte").SvelteComponent<Props, Events, Slots>>;
immutable?: boolean | undefined;
hydrate?: boolean | undefined;
recover?: boolean | undefined;
}): SvelteComponent<Props, Events, Slots> & Exports;
}): import("svelte").SvelteComponent<Props, Events, Slots> & Exports;
/**
* Takes the component function and returns a Svelte 4 compatible component constructor.
*
* @deprecated Use this only as a temporary solution to migrate your imperative component code to Svelte 5.
*
* */
export function asClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(component: SvelteComponent<Props, Events, Slots>): ComponentType<SvelteComponent<Props, Events, Slots> & Exports>;
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
/**
* @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore.
* Use `mount` or `createRoot` instead to instantiate components.
* See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes)
* for more info.
*/
interface ComponentConstructorOptions<
Props extends Record<string, any> = Record<string, any>
> {
target: Element | Document | ShadowRoot;
anchor?: Element;
props?: Props;
context?: Map<any, any>;
hydrate?: boolean;
intro?: boolean;
$$inline?: boolean;
}
// Utility type for ensuring backwards compatibility on a type level: If there's a default slot, add 'children' to the props if it doesn't exist there already
type PropsWithChildren<Props, Slots> = Props &
(Props extends { children?: any }
? {}
: Slots extends { default: any }
? { children?: Snippet }
: {});
/**
* Can be used to create strongly typed Svelte components.
*
* #### Example:
*
* You have component library on npm called `component-library`, from which
* you export a component called `MyComponent`. For Svelte+TypeScript users,
* you want to provide typings. Therefore you create a `index.d.ts`:
* ```ts
* import { SvelteComponent } from "svelte";
* export class MyComponent extends SvelteComponent<{foo: string}> {}
* ```
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
* to provide intellisense and to use the component like this in a Svelte file
* with TypeScript:
* ```svelte
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*
* This was the base class for Svelte components in Svelte 4. Svelte 5+ components
* are completely different under the hood. You should only use this type for typing,
* not actually instantiate components with `new` - use `mount` or `createRoot` instead.
* See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes)
* for more info.
*/
class SvelteComponent<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
[prop: string]: any;
/**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
*/
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: PropsWithChildren<Props, Slots>;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*
* */
$$events_def: Events;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*
* */
$$slot_def: Slots;
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes
* for more info.
*/
$destroy(): void;
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes
* for more info.
*/
$on<K extends Extract<keyof Events, string>>(
type: K,
callback: (e: Events[K]) => void
): () => void;
/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes
* for more info.
*/
$set(props: Partial<Props>): void;
}
/**
* Convenience type to get the type of a Svelte component. Useful for example in combination with
* dynamic components using `<svelte:component>`.
*
* Example:
* ```html
* <script lang="ts">
* import type { ComponentType, SvelteComponent } from 'svelte';
* import Component1 from './Component1.svelte';
* import Component2 from './Component2.svelte';
*
* const component: ComponentType = someLogic() ? Component1 : Component2;
* const componentOfCertainSubType: ComponentType<SvelteComponent<{ needsThisProp: string }>> = someLogic() ? Component1 : Component2;
* </script>
*
* <svelte:component this={component} />
* <svelte:component this={componentOfCertainSubType} needsThisProp="hello" />
* ```
*/
type ComponentType<Comp extends SvelteComponent = SvelteComponent> = (new (
options: ComponentConstructorOptions<
Comp extends SvelteComponent<infer Props> ? Props : Record<string, any>
>
) => Comp) & {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
element?: typeof HTMLElement;
};
const SnippetReturn: unique symbol;
/**
* The type of a `#snippet` block. You can use it to (for example) express that your component expects a snippet of a certain type:
* ```ts
* let { banner }: { banner: Snippet<{ text: string }> } = $props();
* ```
* You can only call a snippet through the `{@render ...}` tag.
*/
type Snippet<T extends unknown[] = []> =
// this conditional allows tuples but not arrays. Arrays would indicate a
// rest parameter type, which is not supported. If rest parameters are added
// in the future, the condition can be removed.
number extends T['length']
? never
: {
(
this: void,
...args: T
): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"';
};
};
export function asClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(component: import("svelte").SvelteComponent<Props, Events, Slots>): import("svelte").ComponentType<import("svelte").SvelteComponent<Props, Events, Slots> & Exports>;
}
declare module 'svelte/motion' {
@ -1971,6 +1806,48 @@ declare module 'svelte/motion' {
export function tweened<T>(value?: T | undefined, defaults?: TweenedOptions<T> | undefined): Tweened<T>;
}
declare module 'svelte/reactivity' {
export class Date extends Date {
constructor(...values: any[]);
#private;
}
export class Set<T> extends Set<any> {
constructor(value?: Iterable<T> | null | undefined);
has(value: T): boolean;
add(value: T): this;
delete(value: T): boolean;
keys(): IterableIterator<T>;
values(): IterableIterator<T>;
entries(): IterableIterator<[T, T]>;
[Symbol.iterator](): IterableIterator<T>;
#private;
}
export class Map<K, V> extends Map<any, any> {
constructor(value?: Iterable<readonly [K, V]> | null | undefined);
has(key: K): boolean;
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, this_arg?: any): void;
get(key: K): V | undefined;
set(key: K, value: V): this;
delete(key: K): boolean;
keys(): IterableIterator<K>;
values(): IterableIterator<V>;
entries(): IterableIterator<[K, V]>;
[Symbol.iterator](): IterableIterator<[K, V]>;
#private;
}
}
declare module 'svelte/server' {
export function render(component: (...args: any[]) => void, options: {
props: Record<string, any>;

@ -93,6 +93,25 @@ This can improve performance with large arrays and objects that you weren't plan
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead.
### Reactive Map, Set and Date
Svelte provides reactive `Map`, `Set` and `Date` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts.
```svelte
<script>
import { Map } from 'svelte/reactivity';
const map = new Map();
map.set('message', 'hello');
function update_message() {
map.set('message', 'goodbye');
}
</script>
<p>{map.get('message')}</p>
```
## `$derived`
Derived state is declared with the `$derived` rune:

@ -103,7 +103,17 @@ No. You can do the migration towards runes incrementally when Svelte 5 comes out
### When can I `npm install` the Svelte 5 preview?
We plan to publish a pre-release version with enough time for brave souls to try it out in their apps and give us feedback on what breaks. Watch this space.
Right now!
```bash
npm install svelte@next
```
You can also opt into Svelte 5 when creating a new SvelteKit project:
```bash
npm create svelte@latest
```
### What's left to do?
@ -119,7 +129,7 @@ We know that some of you are very keen on certain feature ideas, and we are too.
### I want to help. How do I contribute?
We appreciate your enthusiasm! Right now it's not possible to accept contributions, but once we enter public beta, everything will be available on the Svelte GitHub repository.
We appreciate your enthusiasm! We welcome issues on the [sveltejs/svelte](https://github.com/sveltejs/svelte) repo. Pull requests are a little dicier right now since many things are in flux, so we recommended starting with an issue.
### How can I share feedback or cool examples of what this enables?

Loading…
Cancel
Save