Merge branch 'main' into use-get-descriptors

pull/9389/head
Rich Harris 3 years ago
commit 085f9029a5

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: remove selector api

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correct update_block index type

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: tighten up signals implementation

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve keyblock treeshaking

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: remove Component type, keep using SvelteComponent instead

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add snippet marker symbol to children prop

@ -1,6 +1,8 @@
## Svelte compiler rewrite
## Svelte 5 rewrite
Please note that [the Svelte codebase is currently being rewritten](https://svelte.dev/blog/runes). Thus, it's best to hold off on new features or refactorings for the time being.
Please note that [the Svelte codebase is currently being rewritten for Svelte 5](https://svelte.dev/blog/runes). Changes should target Svelte 5, which lives on the default branch (`main`).
If your PR concerns Svelte 4 (including updates to [svelte.dev.docs](https://svelte.dev/docs)), please ensure the base branch is `svelte-4` and not `main`.
### Before submitting the PR, please make sure you do the following

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

@ -251,6 +251,8 @@ export const function_visitor = (node, context) => {
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
state = { ...context.state, in_constructor };
} else {
state = { ...context.state, in_constructor: false };
}
if (metadata?.hoistable === true) {

@ -819,15 +819,21 @@ function serialize_inline_component(node, component_name, context) {
const body = create_block(node, `${node.name}_${slot_name}`, children[slot_name], context);
if (body.length === 0) continue;
const fn = b.arrow(
const slot_fn = b.arrow(
[b.id('$$anchor'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' ? default_lets : []), ...body])
);
if (slot_name === 'default') {
push_prop(b.prop('init', b.id('children'), fn));
push_prop(
b.prop(
'init',
b.id('children'),
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
} else {
serialized_slots.push(b.prop('init', b.key(slot_name), fn));
serialized_slots.push(b.prop('init', b.key(slot_name), slot_fn));
}
}
@ -2221,19 +2227,34 @@ export const template_visitors = {
declarations.push(b.let(node.index, index));
}
context.state.after_update.push(
b.stmt(
b.call(
'$.each',
context.state.node,
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.literal(each_type),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))),
else_block
if ((each_type & EACH_KEYED) !== 0) {
context.state.after_update.push(
b.stmt(
b.call(
'$.each_keyed',
context.state.node,
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.literal(each_type),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))),
else_block
)
)
)
);
);
} else {
context.state.after_update.push(
b.stmt(
b.call(
'$.each_indexed',
context.state.node,
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.literal(each_type),
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))),
else_block
)
)
);
}
},
IfBlock(node, context) {
context.state.template.push('<!>');

@ -847,26 +847,21 @@ function serialize_inline_component(node, component_name, context) {
const body = create_block(node, children[slot_name], context);
if (body.length === 0) continue;
const slot_fn = b.arrow(
[b.id('$$payload'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' ? default_lets : []), ...body])
);
if (slot_name === 'default') {
push_prop(
b.prop(
'init',
b.id('children'),
b.arrow(
[b.id('$$payload'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' ? default_lets : []), ...body])
)
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
} else {
const slot = b.prop(
'init',
b.literal(slot_name),
b.arrow(
[b.id('$$payload'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' ? default_lets : []), ...body])
)
);
const slot = b.prop('init', b.literal(slot_name), slot_fn);
serialized_slots.push(slot);
}
}

@ -8,21 +8,19 @@ const has_browser_globals = typeof window !== 'undefined';
// than megamorphic.
const node_prototype = /** @type {Node} */ (has_browser_globals ? Node.prototype : {});
const element_prototype = /** @type {Element} */ (has_browser_globals ? Element.prototype : {});
const event_target_prototype = /** @type {EventTarget} */ (
has_browser_globals ? EventTarget.prototype : {}
);
const text_prototype = /** @type {Text} */ (has_browser_globals ? Text.prototype : {});
const map_prototype = Map.prototype;
const append_child_method = node_prototype.appendChild;
const clone_node_method = node_prototype.cloneNode;
const map_set_method = map_prototype.set;
const map_get_method = map_prototype.get;
const map_delete_method = map_prototype.delete;
// @ts-expect-error improve perf of expando on DOM nodes for events
event_target_prototype.__click = undefined;
// @ts-expect-error improve perf of expando on DOM textValue updates
event_target_prototype.__nodeValue = ' ';
// @ts-expect-error improve perf of expando on DOM events
element_prototype.__click = undefined;
// @ts-expect-error improve perf of expando on DOM text updates
text_prototype.__nodeValue = ' ';
// @ts-expect-error improve perf of expando on DOM className updates
event_target_prototype.__className = '';
element_prototype.__className = '';
const first_child_get = /** @type {(this: Node) => ChildNode | null} */ (
// @ts-ignore
@ -162,11 +160,10 @@ export function set_class_name(node, class_name) {
/**
* @template {Node} N
* @param {N} node
* @param {string} text
* @returns {void}
*/
export function text_content(node, text) {
text_content_set.call(node, text);
export function clear_text_content(node) {
text_content_set.call(node, '');
}
/** @param {string} name */

@ -1,4 +1,4 @@
import { append_child, map_get, map_set, text_content } from './operations.js';
import { append_child, map_get, map_set, clear_text_content } from './operations.js';
import {
current_hydration_fragment,
get_hydration_fragment,
@ -198,7 +198,7 @@ export function reconcile_indexed_array(
b_blocks = [];
// Remove old blocks
if (is_controlled && a !== 0) {
text_content(dom, '');
clear_text_content(dom);
}
while (index < length) {
block = a_blocks[index++];
@ -260,9 +260,9 @@ export function reconcile_indexed_array(
* @param {Element | Comment | Text} dom
* @param {boolean} is_controlled
* @param {(anchor: null, item: V, index: number | import('./types.js').Signal<number>) => void} render_fn
* @param {Array<string> | null} keys
* @param {number} flags
* @param {boolean} apply_transitions
* @param {Array<string> | null} keys
* @returns {void}
*/
export function reconcile_tracked_array(
@ -271,9 +271,9 @@ export function reconcile_tracked_array(
dom,
is_controlled,
render_fn,
keys,
flags,
apply_transitions
apply_transitions,
keys
) {
var a_blocks = each_block.items;
const is_computed_key = keys !== null;
@ -295,7 +295,7 @@ export function reconcile_tracked_array(
b_blocks = [];
// Remove old blocks
if (is_controlled && a !== 0) {
text_content(dom, '');
clear_text_content(dom);
}
while (a > 0) {
block = a_blocks[--a];

@ -68,7 +68,7 @@ import {
hydrate_block_anchor,
set_current_hydration_fragment
} from './hydration.js';
import { array_from, define_property, get_descriptor, get_descriptors, is_array } from './utils.js';
import { array_from, define_property, get_descriptor, is_array } from './utils.js';
import { is_promise } from '../common.js';
import { bind_transition } from './transitions.js';
@ -1270,15 +1270,15 @@ function handle_event_propagation(root_element, event) {
}
// composedPath contains list of nodes the event has propagated through.
// We check __handled_event_at to skip all nodes below it in case this is a
// parent of the __handled_event_at node, which indicates that there's nested
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
// We're deliberately not skipping if the index is the same or higher, because
// someone could create an event programmatically and emit it multiple times,
// in which case we want to handle the whole propagation chain properly each time.
let path_idx = 0;
// @ts-expect-error is added below
const handled_at = event.__handled_event_at;
const handled_at = event.__root;
if (handled_at) {
const at_idx = path.indexOf(handled_at);
if (at_idx < path.indexOf(root_element)) {
@ -1317,7 +1317,7 @@ function handle_event_propagation(root_element, event) {
}
// @ts-expect-error is used above
event.__handled_event_at = root_element;
event.__root = root_element;
}
/**
@ -2078,7 +2078,7 @@ function get_first_element(block) {
/**
* @param {import('./types.js').EachItemBlock} block
* @param {any} item
* @param {import('./types.js').MaybeSignal<number>} index
* @param {number} index
* @param {number} type
* @returns {void}
*/
@ -2093,7 +2093,6 @@ export function update_each_item_block(block, item, index, type) {
let prev_index = block.index;
if (index_is_reactive) {
prev_index = /** @type {import('./types.js').Signal<number>} */ (prev_index).value;
index = /** @type {import('./types.js').Signal<number>} */ (index).value;
}
const items = block.parent.items;
if (prev_index !== index && /** @type {number} */ (index) < items.length) {
@ -2263,9 +2262,10 @@ export function each_item_block(item, key, index, render_fn, flags) {
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
*/
export function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn) {
function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_fn) {
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
const block = create_each_block(flags, anchor_node);
@ -2385,20 +2385,7 @@ export function each(anchor_node, collection, flags, key_fn, render_fn, fallback
const flags = block.flags;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
const anchor_node = block.anchor;
if ((flags & EACH_KEYED) !== 0) {
reconcile_tracked_array(
array,
block,
anchor_node,
is_controlled,
render_fn,
keys,
flags,
true
);
} else {
reconcile_indexed_array(array, block, anchor_node, is_controlled, render_fn, flags, true);
}
reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys);
},
block,
true
@ -2420,12 +2407,39 @@ export function each(anchor_node, collection, flags, key_fn, render_fn, fallback
fallback = fallback.prev;
}
// Clear the array
reconcile_indexed_array([], block, anchor_node, is_controlled, render_fn, flags, false);
reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys);
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (render));
});
block.effect = each;
}
/**
* @template V
* @param {Element | Comment} anchor_node
* @param {() => V[]} collection
* @param {number} flags
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fallback_fn) {
each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_tracked_array);
}
/**
* @template V
* @param {Element | Comment} anchor_node
* @param {() => V[]} collection
* @param {number} flags
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
export function each_indexed(anchor_node, collection, flags, render_fn, fallback_fn) {
each(anchor_node, collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array);
}
/**
* @param {Element | Text | Comment} anchor
* @param {boolean} is_html
@ -3020,7 +3034,7 @@ export function unwrap(value) {
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').Component<Props, Exports, Events>} component
* @param {import('../../main/public.js').SvelteComponent<Props, Events>} component
* @param {{
* target: Node;
* props?: Props;
@ -3139,7 +3153,7 @@ export function createRoot(component, options) {
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').Component<Props, Exports, Events>} component
* @param {import('../../main/public.js').SvelteComponent<Props, Events>} component
* @param {{
* target: Node;
* props?: Props;

@ -343,7 +343,13 @@ function destroy_references(signal) {
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
destroy_signal(references[i]);
const reference = references[i];
if ((reference.flags & IS_EFFECT) !== 0) {
destroy_signal(reference);
} else {
remove_consumer(reference, 0, true);
reference.dependencies = null;
}
}
}
}
@ -710,7 +716,7 @@ export function exposable(fn) {
export function get(signal) {
const flags = signal.flags;
if ((flags & DESTROYED) !== 0) {
return /** @type {V} */ (UNINITIALIZED);
return signal.value;
}
if (is_signal_exposed && current_should_capture_signal) {
@ -1156,6 +1162,11 @@ export function managed_pre_effect(init, sync) {
* @returns {import('./types.js').EffectSignal}
*/
export function pre_effect(init) {
if (current_effect === null) {
throw new Error(
'The Svelte $effect.pre rune can only be used during component initialisation.'
);
}
const sync = current_effect !== null && (current_effect.flags & RENDER_EFFECT) !== 0;
return internal_create_effect(
PRE_EFFECT,
@ -1243,105 +1254,6 @@ export function set_signal_status(signal, status) {
}
}
/** @template V */
class Selector {
/** @type {Map<V, Set<import('./types.js').Signal>>} */
#consumers_map = new Map();
/** @type {import('./types.js').Signal<V | null>} */
#active_key;
/** @param {V | null} [key] */
constructor(key) {
this.#active_key = source(key || null);
}
get current() {
return get(this.#active_key);
}
/**
* @param {V | null} key
* @returns {void}
*/
set(key) {
const active_key = this.#active_key;
const previous_key = active_key.value;
if (previous_key === key) {
return;
}
set_signal_value(active_key, key);
const consumers_map = this.#consumers_map;
let consumers = map_get(consumers_map, /** @type {V} */ (previous_key));
if (consumers !== undefined) {
this.#update_consumers(consumers);
}
consumers = map_get(consumers_map, /** @type {V} */ (key));
if (consumers !== undefined) {
this.#update_consumers(consumers);
}
}
/**
* @param {Set<import('./types.js').Signal>} consumers
* @returns {void}
*/
#update_consumers(consumers) {
let consumer;
for (consumer of consumers) {
set_signal_status(consumer, DIRTY);
if ((consumer.flags & IS_EFFECT) !== 0) {
schedule_effect(/** @type {import('./types.js').EffectSignal} */ (consumer), false);
} else {
mark_signal_consumers(consumer, DIRTY, true);
}
}
}
/**
* @param {V} key
* @returns {boolean}
*/
is(key) {
const consumers_map = this.#consumers_map;
let consumers = map_get(consumers_map, key);
if (consumers === undefined) {
consumers = new Set();
map_set(consumers_map, key, consumers);
}
const consumer = current_consumer;
const effect = current_effect;
if (effect !== null && consumer !== null && !consumers.has(consumer)) {
consumers.add(consumer);
push_destroy_fn(effect, () => {
const consumers_set = /** @type {Set<import('./types.js').Signal>} */ (consumers);
consumers_set.delete(effect);
if (consumers_set.size === 0) {
map_delete(consumers_map, key);
}
});
}
return this.#active_key.value === key;
}
}
/**
* `selector` allows you to track the currently selected item in a list in a performance optimized manner
* that runs in constant time (O(1)) - this is only noticable for very large lists.
*
* https://svelte-5-preview.vercel.app/docs/functions#selector
* @template Key
* @param {Key | null} [key]
* @returns {Selector<Key>}
*/
export function selector(key) {
return new Selector(key);
}
/**
* @template V
* @param {V | import('./types.js').Signal<V>} val

@ -119,7 +119,7 @@ export function add_snippet_symbol(fn) {
export function validate_snippet(snippet_fn) {
if (snippet_fn[symbol] !== true) {
throw new Error(
'The argument to `{@html ...}` must be a snippet function, not a component or some other kind of function. ' +
'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' +
'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.'
);
}

@ -13,7 +13,6 @@ export {
user_effect,
render_effect,
pre_effect,
selector,
flushSync,
bubble_event,
safe_equal,

@ -11,12 +11,12 @@ import * as $ from '../internal/index.js';
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('./public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').Component<Props, Exports, Events, Slots>;
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
* immutable?: boolean;
* recover?: false;
* }} options
* @returns {import('./public.js').SvelteComponent<Props & Exports, Events, Slots>}
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
*/
export function createClassComponent(options) {
// @ts-expect-error $$prop_def etc are not actually defined
@ -33,8 +33,8 @@ export function createClassComponent(options) {
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('../main/public.js').Component<Props, Exports, Events, Slots>} component
* @returns {typeof import('./public.js').SvelteComponent<Props & Exports, Events, Slots>}
* @param {import('../main/public.js').SvelteComponent<Props, Events, Slots>} component
* @returns {typeof import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
*/
export function asClassComponent(component) {
// @ts-expect-error $$prop_def etc are not actually defined
@ -57,7 +57,7 @@ class Svelte4Component {
#instance;
/**
* @param {import('./public.js').ComponentConstructorOptions & {
* @param {import('../main/public.js').ComponentConstructorOptions & {
* component: any;
* immutable?: boolean;
* recover?: false;

@ -6,7 +6,7 @@ import { render } from '../internal/server/index.js';
export { createClassComponent };
/**
* Takes the component function and returns a Svelte 4 compatible component constructor.
* Takes a Svelte 5 component 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.
*
@ -15,8 +15,8 @@ export { createClassComponent };
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('../main/public.js').Component<Props, Exports, Events, Slots>} component
* @returns {typeof import('./public.js').SvelteComponent<Props & Exports, Events, Slots>}
* @param {import('../main/public.js').SvelteComponent<Props, Events, Slots>} component
* @returns {typeof import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
*/
export function asClassComponent(component) {
const component_constructor = as_class_component(component);
@ -30,8 +30,9 @@ export function asClassComponent(component) {
html: result.html
};
};
// @ts-expect-error this is present for SSR
// this is present for SSR
component_constructor.render = _render;
// @ts-ignore
return component_constructor;
}

@ -1,93 +0,0 @@
/**
* @deprecated Use `Component` instead. See TODO for more information.
*/
export 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;
}
/**
* @deprecated use `Component` instead. See TODO for more information.
*
* Base class for Svelte components in Svelte 4. Svelte 5+ components implement
* the `Component` interface instead. This class is only provided for backwards
* compatibility with Svelte 4 typings and doesn't have any runtime equivalent.
*
* 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'} />
* ```
*/
export class SvelteComponent<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
[prop: string]: any;
constructor(options: ComponentConstructorOptions<Props>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*
* */
$$prop_def: Props;
/**
* 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;
$destroy(): void;
$on<K extends Extract<keyof Events, string>>(
type: K,
callback: (e: Events[K]) => void
): () => void;
$set(props: Partial<Props>): void;
}
/**
* @deprecated Use `Component` instead. See TODO for more information.
*/
export class SvelteComponentTyped<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends SvelteComponent<Props, Events, Slots> {}
export * from './legacy-client.js';

@ -1,5 +1,5 @@
declare module '*.svelte' {
export { Component as default } from 'svelte';
export { SvelteComponent as default } from 'svelte';
}
/**

@ -255,12 +255,4 @@ export function afterUpdate(fn) {
// TODO bring implementations in here
// (except probably untrack — do we want to expose that, if there's also a rune?)
export {
flushSync,
createRoot,
mount,
tick,
untrack,
onDestroy,
selector
} from '../internal/index.js';
export { flushSync, createRoot, mount, tick, untrack, onDestroy } from '../internal/index.js';

@ -7,7 +7,6 @@ export {
hasContext,
mount,
onDestroy,
selector,
setContext,
tick,
untrack

@ -1,18 +1,24 @@
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
// Once we convert to JSDoc make it a d.ts file.
import type {
ComponentConstructorOptions,
SvelteComponent,
SvelteComponentTyped
} from '../legacy/public.js';
// For Svelte 6 we can think about only exporting these from svelte/legacy
export { SvelteComponent, SvelteComponentTyped, ComponentConstructorOptions };
/**
* @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.
*/
export 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;
}
/**
* Base interface for Svelte components.
*
* Can be used to create strongly typed Svelte components.
*
* #### Example:
@ -21,8 +27,8 @@ export { SvelteComponent, SvelteComponentTyped, ComponentConstructorOptions };
* you export a component called `MyComponent`. For Svelte+TypeScript users,
* you want to provide typings. Therefore you create a `index.d.ts`:
* ```ts
* import type { Component } from "svelte";
* export type MyComponent = Component<{foo: string}>
* 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
@ -33,27 +39,87 @@ export { SvelteComponent, SvelteComponentTyped, ComponentConstructorOptions };
* </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.
*/
export interface Component<
Props extends Record<string, any> = {},
Exports extends Record<string, any> | undefined = undefined,
Events extends Record<string, any> = {},
Slots extends Record<string, any> = {}
export class SvelteComponent<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
element?: typeof HTMLElement;
[prop: string]: any;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
constructor(props: Props);
/**
* @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<Props>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: Props;
/**
* 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;
/**
* ## DO NOT USE THIS
* This only exists for typing purposes and has no runtime value.
* @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.
*/
z_$$(
props: Props,
events: Events,
slots: Slots
): Exports extends undefined ? Props | undefined : Exports & Partial<Props>;
$set(props: Partial<Props>): void;
}
/**
* @deprecated Use `SvelteComponent` instead. See TODO for more information.
*/
export class SvelteComponentTyped<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends SvelteComponent<Props, Events, Slots> {}
/**
* Convenience type to get the events the given component expects. Example:
* ```html
@ -69,12 +135,12 @@ export interface Component<
* <Component on:close={handleCloseEvent} />
* ```
*/
export type ComponentEvents<Comp extends Component<any, any, any, any> | SvelteComponent> =
Comp extends SvelteComponent<any, infer Events>
? Events
: Comp extends Component<any, any, infer Events, any>
? Events
: never;
export type ComponentEvents<Comp extends SvelteComponent> = Comp extends SvelteComponent<
any,
infer Events
>
? Events
: never;
/**
* Convenience type to get the props the given component expects. Example:
@ -87,16 +153,12 @@ export type ComponentEvents<Comp extends Component<any, any, any, any> | SvelteC
* </script>
* ```
*/
export type ComponentProps<Comp extends Component<any, any, any, any> | SvelteComponent> =
Comp extends SvelteComponent<infer Props>
? Props
: Comp extends Component<infer Props, any, any, any>
? Props
: never;
export type ComponentProps<Comp extends SvelteComponent> = Comp extends SvelteComponent<infer Props>
? Props
: never;
/**
* Convenience type to get the type of a Svelte component. Not necessary when using the `Component` type,
* but useful when using the deprecated `SvelteComponent` type and for example in combination with
* Convenience type to get the type of a Svelte component. Useful for example in combination with
* dynamic components using `<svelte:component>`.
*
* Example:
@ -114,17 +176,14 @@ export type ComponentProps<Comp extends Component<any, any, any, any> | SvelteCo
* <svelte:component this={componentOfCertainSubType} needsThisProp="hello" />
* ```
*/
export type ComponentType<Comp extends Component<any, any, any, any> | SvelteComponent> =
Comp extends 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;
}
: Comp;
export type ComponentType<Comp extends 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;
};
interface DispatchOptions {
cancelable?: boolean;

@ -7,6 +7,48 @@ function cubic_out(t) {
return f * f * f + 1.0;
}
/**
* https://svelte.dev/docs/svelte-easing
* @param {number} t
* @returns {number}
*/
export function cubic_in_out(t) {
return t < 0.5 ? 4.0 * t * t * t : 0.5 * Math.pow(2.0 * t - 2.0, 3.0) + 1.0;
}
/** @param {number | string} value
* @returns {[number, string]}
*/
export function split_css_unit(value) {
const split = typeof value === 'string' && value.match(/^\s*(-?[\d.]+)([^\s]*)\s*$/);
return split ? [parseFloat(split[1]), split[2] || 'px'] : [/** @type {number} */ (value), 'px'];
}
/**
* Animates a `blur` filter alongside an element's opacity.
*
* https://svelte.dev/docs/svelte-transition#blur
* @param {Element} node
* @param {import('./public').BlurParams} [params]
* @returns {import('./public').TransitionConfig}
*/
export function blur(
node,
{ delay = 0, duration = 400, easing = cubic_in_out, amount = 5, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity);
const [value, unit] = split_css_unit(amount);
return {
delay,
duration,
easing,
css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * value}${unit});`
};
}
/**
* Animates the opacity of an element from 0 to the current opacity for `in` transitions and from the current opacity to 0 for `out` transitions.
*
@ -25,6 +67,34 @@ export function fade(node, { delay = 0, duration = 400, easing = linear } = {})
};
}
/**
* Animates the x and y positions and the opacity of an element. `in` transitions animate from the provided values, passed as parameters to the element's default values. `out` transitions animate from the element's default values to the provided values.
*
* https://svelte.dev/docs/svelte-transition#fly
* @param {Element} node
* @param {import('./public').FlyParams} [params]
* @returns {import('./public').TransitionConfig}
*/
export function fly(
node,
{ delay = 0, duration = 400, easing = cubic_out, x = 0, y = 0, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
const od = target_opacity * (1 - opacity);
const [x_value, x_unit] = split_css_unit(x);
const [y_value, y_unit] = split_css_unit(y);
return {
delay,
duration,
easing,
css: (t, u) => `
transform: ${transform} translate(${(1 - t) * x_value}${x_unit}, ${(1 - t) * y_value}${y_unit});
opacity: ${target_opacity - od * u}`
};
}
/**
* Slides an element in and out.
*
@ -69,6 +139,68 @@ export function slide(node, { delay = 0, duration = 400, easing = cubic_out, axi
};
}
/**
* Animates the opacity and scale of an element. `in` transitions animate from an element's current (default) values to the provided values, passed as parameters. `out` transitions animate from the provided values to an element's default values.
*
* https://svelte.dev/docs/svelte-transition#scale
* @param {Element} node
* @param {import('./public').ScaleParams} [params]
* @returns {import('./public').TransitionConfig}
*/
export function scale(
node,
{ delay = 0, duration = 400, easing = cubic_out, start = 0, opacity = 0 } = {}
) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
const sd = 1 - start;
const od = target_opacity * (1 - opacity);
return {
delay,
duration,
easing,
css: (_t, u) => `
transform: ${transform} scale(${1 - sd * u});
opacity: ${target_opacity - od * u}
`
};
}
/**
* Animates the stroke of an SVG element, like a snake in a tube. `in` transitions begin with the path invisible and draw the path to the screen over time. `out` transitions start in a visible state and gradually erase the path. `draw` only works with elements that have a `getTotalLength` method, like `<path>` and `<polyline>`.
*
* https://svelte.dev/docs/svelte-transition#draw
* @param {SVGElement & { getTotalLength(): number }} node
* @param {import('./public').DrawParams} [params]
* @returns {import('./public').TransitionConfig}
*/
export function draw(node, { delay = 0, speed, duration, easing = cubic_in_out } = {}) {
let len = node.getTotalLength();
const style = getComputedStyle(node);
if (style.strokeLinecap !== 'butt') {
len += parseInt(style.strokeWidth);
}
if (duration === undefined) {
if (speed === undefined) {
duration = 800;
} else {
duration = len / speed;
}
} else if (typeof duration === 'function') {
duration = duration(len);
}
return {
delay,
duration,
easing,
css: (_, u) => `
stroke-dasharray: ${len};
stroke-dashoffset: ${u * len};
`
};
}
/**
* @template T
* @template S

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,
async test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});

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

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,
async test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});

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

@ -0,0 +1,19 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
get props() {
return { log: [] };
},
async test({ assert, target, component }) {
const [b1] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
});
flushSync(() => {
b1.click();
});
assert.deepEqual(component.log, ['init 0', 'cleanup 2', 'init 2', 'cleanup 4', 'init 4']);
}
});

@ -0,0 +1,17 @@
<script>
const {log} = $props();
let count = $state(0);
$effect(() => {
let double = $derived(count * 2)
log.push('init ' + double);
return () => {
log.push('cleanup ' + double);
};
})
</script>
<button on:click={() => count++ }>Click</button>

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

@ -1,11 +0,0 @@
<script>
import { selector } from 'svelte';
let array = $state([1, 2, 3]);
let selected = selector();
</script>
{#each array as item}
<button on:click={() => selected.set(item)}>{selected.is(item)}</button>
{/each}
<p>{selected.current}</p>

@ -5,6 +5,6 @@ export default test({
dev: true
},
error:
'The argument to `{@html ...}` must be a snippet function, not a component or some other kind of function. ' +
'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' +
'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.'
});

@ -1,7 +1,7 @@
import { SvelteComponent, asClassComponent, createClassComponent } from 'svelte/legacy';
import { asClassComponent, createClassComponent } from 'svelte/legacy';
import {
createRoot,
type Component,
SvelteComponent,
type ComponentEvents,
type ComponentProps,
type ComponentType
@ -15,11 +15,11 @@ class LegacyComponent extends SvelteComponent<
{ slot: { slotProps: boolean } }
> {}
// @ts-expect-error
const legacyComponent = new LegacyComponent({
target: null as any as Document | Element | ShadowRoot,
props: {
prop: 'foo',
// @ts-expect-error
x: ''
}
});
@ -48,35 +48,34 @@ const legacyComponentEvents2: ComponentEvents<LegacyComponent> = {
// --------------------------------------------------------------------------- new: functions
type NewComponent = Component<
class NewComponent extends SvelteComponent<
{ prop: string },
{ anExport: number },
{ event: MouseEvent },
{ slot: { slotProps: boolean } }
>;
const newComponent: NewComponent = {
z_$$: (props, events, slots) => {
props.prop;
// @ts-expect-error
props.x;
> {
anExport: string = '';
}
events.event;
// @ts-expect-error
events.x;
slots.slot;
// @ts-expect-error
slots.x;
// @ts-expect-error
new NewComponent({
prop: 'foo',
x: ''
});
return {
anExport: 1,
prop: props.prop
};
}
};
const newComponent: NewComponent = new NewComponent({
prop: 'foo'
});
newComponent.$$events_def.event;
// @ts-expect-error
newComponent.$$events_def.x;
newComponent.$$slot_def.slot;
// @ts-expect-error
newComponent.$$slot_def.x;
newComponent.anExport === '';
// @ts-expect-error
newComponent.anExport === 1;
const newComponentType: ComponentType<NewComponent> = newComponent;
const newComponentType: ComponentType<NewComponent> = NewComponent;
const newComponentProps1: ComponentProps<NewComponent> = {
prop: '',
@ -124,20 +123,28 @@ instance.anExport === 1;
// --------------------------------------------------------------------------- interop
const AsLegacyComponent = asClassComponent(newComponent);
const asLegacyComponent = new AsLegacyComponent({
// @ts-expect-error
new AsLegacyComponent({
target: null as any,
props: {
prop: '',
// @ts-expect-error
x: ''
}
});
const asLegacyComponent = new AsLegacyComponent({
target: null as any,
props: {
prop: ''
}
});
asLegacyComponent.$on('event', (e) => e.clientX);
// @ts-expect-error
asLegacyComponent.$on('event', (e) => e.foo);
// @ts-expect-error
asLegacyComponent.$on('bar', (e) => e);
asLegacyComponent.$$prop_def.prop = '';
asLegacyComponent.anExport = '';
// @ts-expect-error
asLegacyComponent.$$prop_def.anExport = 1;
// @ts-expect-error
asLegacyComponent.$$prop_def.prop = 1;

@ -19,7 +19,7 @@
"svelte/action": ["./src/action/public.d.ts"],
"svelte/compiler": ["./src/compiler/public.d.ts"],
"svelte/internal": ["./src/internal/index.js"],
"svelte/legacy": ["./src/legacy/public.d.ts"],
"svelte/legacy": ["./src/legacy/legacy-client.js"],
"svelte/server": ["./src/server/index.js"],
"svelte/store": ["./src/store/public.d.ts"],
"#compiler": ["./src/compiler/types/index.d.ts"]

@ -1,6 +1,7 @@
<!-- svelte-ignore a11y-label-has-associated-control -->
<script>
import { get_repl_context } from '$lib/context.js';
import Checkbox from '../Checkbox.svelte';
const { compile_options } = get_repl_context();
</script>
@ -16,6 +17,12 @@
<input id="server" type="radio" bind:group={$compile_options.generate} value="server" />
<label for="server"><span class="string">"server"</span>,</label>
</div>
<label class="option">
<span class="key">dev:</span>
<Checkbox bind:checked={$compile_options.dev} />
<span class="boolean">{$compile_options.dev}</span>,
</label>
});
</div>

@ -76,7 +76,8 @@
/** @type {import('svelte/compiler').CompileOptions} */
const DEFAULT_COMPILE_OPTIONS = {
generate: 'client'
generate: 'client',
dev: false
};
/** @type {Map<string, import('@codemirror/state').EditorState>} */

@ -387,7 +387,8 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
} else if (id.endsWith('.svelte')) {
result = svelte.compile(code, {
filename: name + '.svelte',
generate: 'client'
generate: 'client',
dev: true
});
if (result.css) {
@ -400,7 +401,11 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
`.replace(/\t/g, '');
}
} else if (id.endsWith('.svelte.js')) {
result = svelte.compileModule(code, { filename: name + '.js', generate: 'client' });
result = svelte.compileModule(code, {
filename: name + '.js',
generate: 'client',
dev: true
});
if (!result) {
return null;
}
@ -420,7 +425,7 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
warnings.push(warning);
});
/** @type {import('rollup').TransformResult} */
/** @type {import('@rollup/browser').TransformResult} */
const transform_result = {
code: result.js.code,
map: result.js.map

@ -59,7 +59,8 @@ function compile({ id, source, options, return_ast }) {
if (options.filename.endsWith('.svelte')) {
const compiled = svelte.compile(source, {
filename: options.filename,
generate: options.generate
generate: options.generate,
dev: options.dev
});
const { js, css, warnings, metadata } = compiled;
@ -77,7 +78,8 @@ function compile({ id, source, options, return_ast }) {
} else if (options.filename.endsWith('.svelte.js')) {
const compiled = svelte.compileModule(source, {
filename: options.filename,
generate: options.generate
generate: options.generate,
dev: options.dev
});
if (compiled) {

@ -0,0 +1,9 @@
{
"include": ["./**/*"],
"compilerOptions": {
"paths": {
"svelte": ["../../../static/svelte/main"],
"svelte/*": ["../../../static/svelte/*"]
}
}
}

@ -22,22 +22,3 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
});
</script>
```
## `selector`
`selector` allows you to track the currently selected item in a list in a performance optimized manner that runs in constant time (`O(1)`). With `selector`, you can immediately determine if an item is selected:
```svelte
<script>
import { selector } from 'svelte';
let array = $state([1, 2, 3]);
let selected = selector();
</script>
{#each array as item}
<button on:click={() => selected.set(item)}>{selected.is(item)}</button>
{/each}
<p>{selected.current}</p>
```

Loading…
Cancel
Save