Merge branch 'main' into skip-site-deploy

pull/9424/head
Rich Harris 3 years ago
commit feabe2088d

@ -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

@ -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) {

@ -2227,10 +2227,11 @@ export const template_visitors = {
declarations.push(b.let(node.index, index));
}
if ((each_type & EACH_KEYED) !== 0) {
context.state.after_update.push(
b.stmt(
b.call(
'$.each',
'$.each_keyed',
context.state.node,
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.literal(each_type),
@ -2240,6 +2241,20 @@ export const template_visitors = {
)
)
);
} 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('<!>');

@ -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

@ -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

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

@ -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

@ -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>

@ -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"]

@ -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