Merge branch 'main' into svelte-custom-renderer

svelte-custom-renderer-single-type-argument
Paolo Ricciuti 3 weeks ago committed by GitHub
commit 8a2e0584cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly compile component member expressions for SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reset `source.updated` stack traces after `flush`

@ -9,5 +9,9 @@ import { build_inline_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
build_inline_component(node, /** @type {Expression} */ (context.visit(b.id(node.name))), context);
build_inline_component(
node,
/** @type {Expression} */ (context.visit(b.member_id(node.name))),
context
);
}

@ -85,7 +85,9 @@ export let collected_effects = null;
export let legacy_updates = null;
var flush_count = 0;
var source_stacks = DEV ? new Set() : null;
/** @type {Set<Value>} */
var source_stacks = new Set();
let uid = 1;
@ -273,6 +275,14 @@ export class Batch {
infinite_loop_guard();
}
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
for (const value of this.current.keys()) {
source_stacks.add(value);
}
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
@ -334,26 +344,28 @@ export class Batch {
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
} else {
if (this.#pending === 0) {
batches.delete(this);
if (updates.length > 0) {
/** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
return;
}
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
// append/remove branches
for (const fn of this.#commit_callbacks) fn(this);
this.#commit_callbacks.clear();
this.#deferred?.resolve();
}
previous_batch = this;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
@ -361,8 +373,10 @@ export class Batch {
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !batches.has(this)) {
if (async_mode_flag && this.#pending === 0) {
this.#commit();
// Rebases can activate other batches or null it out, therefore restore the new one here
current_batch = next_batch;
}
// Edge case: During traversal new branches might create effects that run immediately and set state,
@ -370,18 +384,11 @@ export class Batch {
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
const batch = (next_batch ??= this);
batches.add(batch);
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
if (next_batch !== null) {
batches.add(next_batch);
if (DEV) {
for (const source of this.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
}
next_batch.#process();
}
}
@ -480,9 +487,11 @@ export class Batch {
}
flush() {
var source_stacks = DEV ? new Set() : null;
try {
if (DEV) {
source_stacks.clear();
}
is_processing = true;
current_batch = this;
@ -500,7 +509,7 @@ export class Batch {
old_values.clear();
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
for (const source of source_stacks) {
source.updated = null;
}
}
@ -523,6 +532,8 @@ export class Batch {
}
#commit() {
batches.delete(this);
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
@ -726,20 +737,14 @@ export class Batch {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batches.add(batch);
if (!is_processing) {
batches.add(current_batch);
if (!is_flushing_sync) {
queue_micro_task(() => {
if (batch.#started) {
// a flushSync happened in the meantime
return;
}
if (!is_processing && !is_flushing_sync) {
queue_micro_task(() => {
if (!batch.#started) {
batch.flush();
});
}
}
});
}
}

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>clicks: 0</button><button>shift</button> 0');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>clicks: 1</button><button>shift</button> 1');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>clicks: 2</button><button>shift</button> 2');
}
});

@ -0,0 +1,25 @@
<script lang="ts">
import { flushSync } from 'svelte';
let count = $state(0);
const queue: Array<() => void> = [];
$effect(() => {
if (count === 1) {
count = 2;
flushSync();
}
})
function push(v: number) {
if (v === 0) return v;
return new Promise(r => queue.push(() => r(v)));
}
</script>
<button onclick={() => count += 1}>
clicks: {count}
</button>
<button onclick={() => queue.shift()?.()}>shift</button>
{await push(count)}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
mode: ['client', 'server'],
html: `<span>x</span>`,
ssrHtml: `<span>x</span>`
});

@ -0,0 +1,8 @@
<script>
import Icon from './Icon.svelte';
let icons = $state({ currency: { Icon } });
const platformIcons = $derived(icons);
</script>
<platformIcons.currency.Icon />

@ -0,0 +1,54 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
// Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null
export default test({
skip_no_async: true,
async test({ target, assert }) {
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>0</p> <button>add watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>1</p> hello 1 <button>remove watcher</button>`
);
const input = target.querySelector('input');
ok(input);
input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> hello 2 <button>remove watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);
input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>3</p> hello 3 <button>remove watcher</button>`
);
}
});

@ -0,0 +1,29 @@
<script>
import { writable, derived } from "svelte/store";
const obj = writable({ a: 1 });
let count = $state(0);
let watcherA = $state();
function watch (prop) {
return derived(obj, (o) => {
count++;
return o[prop];
});
}
</script>
<input type="number" bind:value={$obj.a}>
<p>{count}</p>
{#if watcherA}
<!-- make sure the presence of async work doesn't break the `legacy_updates` mechanism -->
{#if true}
{await 'hello'}
{/if}
{$watcherA}
<button on:click={() => watcherA = null}>remove watcher</button>
{:else}
<button on:click={() => watcherA = watch("a")}>add watcher</button>
{/if}
Loading…
Cancel
Save