pull/18205/head
Rich Harris 2 months ago
commit 799f227a52

@ -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
);
}

@ -88,7 +88,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;
@ -284,6 +286,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()) {
@ -345,29 +355,35 @@ export class Batch {
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
} else {
if (this.#pending === 0) {
this.#unlink();
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));
if (this.linked && this.#pending === 0) {
this.#unlink();
}
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// 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
@ -382,17 +398,16 @@ export class Batch {
// causing an effect and therefore a root to be scheduled again. We need to traverse the current 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);
if (next_batch === null) {
next_batch = this;
this.#link();
}
const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
if (next_batch !== null) {
if (DEV) {
for (const source of this.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
}
next_batch.#process();
}
}
@ -491,9 +506,11 @@ export class Batch {
}
flush() {
var source_stacks = DEV ? new Set() : null;
try {
if (DEV) {
source_stacks.clear();
}
is_processing = true;
current_batch = this;
@ -511,7 +528,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;
}
}
@ -534,6 +551,8 @@ export class Batch {
}
#commit() {
this.#unlink();
// 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
@ -737,27 +756,14 @@ export class Batch {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batch.#link();
if (last_batch === null) {
first_batch = last_batch = batch;
} else {
last_batch.#next = batch;
batch.#prev = last_batch;
}
last_batch = batch;
if (!is_processing) {
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();
});
}
}
});
}
}
@ -865,6 +871,17 @@ export class Batch {
this.#roots.push(e);
}
#link() {
if (last_batch === null) {
first_batch = last_batch = this;
} else {
last_batch.#next = this;
this.#prev = last_batch;
}
last_batch = this;
}
#unlink() {
var prev = this.#prev;
var next = this.#next;

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