Merge branch 'main' into developer-guide

developer-guide
paoloricciuti 3 weeks ago
commit 425e68ac6c

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: run blocks eagerly during flush instead of aborting

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: destroy dynamic component instance before creating new one

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't clone non-proxies in `$inspect`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid recursion error when tagging circular references

@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) {
return; return;
} }
var snap = snapshot(value, true); var snap = snapshot(value, true, true);
untrack(() => { untrack(() => {
inspector(initial ? 'init' : 'update', ...snap); inspector(initial ? 'init' : 'update', ...snap);
}); });

@ -34,11 +34,6 @@ export function component(node, get_component, render_fn) {
var pending_effect = null; var pending_effect = null;
function commit() { function commit() {
if (effect) {
pause_effect(effect);
effect = null;
}
if (offscreen_fragment) { if (offscreen_fragment) {
// remove the anchor // remove the anchor
/** @type {Text} */ (offscreen_fragment.lastChild).remove(); /** @type {Text} */ (offscreen_fragment.lastChild).remove();
@ -56,6 +51,11 @@ export function component(node, get_component, render_fn) {
var defer = should_defer_append(); var defer = should_defer_append();
if (effect) {
pause_effect(effect);
effect = null;
}
if (component) { if (component) {
var target = anchor; var target = anchor;

@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
// export these for reference in the compiled code, making global name deduplication unnecessary // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */
@ -214,6 +215,7 @@ export function clear_text_content(node) {
*/ */
export function should_defer_append() { export function should_defer_append() {
if (!async_mode_flag) return false; if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f; var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0; return (flags & EFFECT_RAN) !== 0;

@ -93,9 +93,11 @@ export function proxy(value) {
/** Used in dev for $inspect.trace() */ /** Used in dev for $inspect.trace() */
var path = ''; var path = '';
let updating = false;
/** @param {string} new_path */ /** @param {string} new_path */
function update_path(new_path) { function update_path(new_path) {
if (updating) return;
updating = true;
path = new_path; path = new_path;
tag(version, `${path} version`); tag(version, `${path} version`);
@ -104,6 +106,7 @@ export function proxy(value) {
for (const [prop, source] of sources) { for (const [prop, source] of sources) {
tag(source, get_label(path, prop)); tag(source, get_label(path, prop));
} }
updating = false;
} }
return new Proxy(/** @type {any} */ (value), { return new Proxy(/** @type {any} */ (value), {
@ -284,13 +287,13 @@ export function proxy(value) {
if (s === undefined) { if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) { if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack)); s = with_parent(() => source(undefined, stack));
set(s, proxy(value));
sources.set(prop, s);
if (DEV) { if (DEV) {
tag(s, get_label(path, prop)); tag(s, get_label(path, prop));
} }
set(s, proxy(value));
sources.set(prop, s);
} }
} else { } else {
has = s.v !== UNINITIALIZED; has = s.v !== UNINITIALIZED;

@ -292,12 +292,12 @@ export class Batch {
if (!skip && effect.fn !== null) { if (!skip && effect.fn !== null) {
if (is_branch) { if (is_branch) {
effect.f ^= CLEAN; effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) { } else if ((flags & CLEAN) === 0) {
if ((flags & EFFECT) !== 0) { if ((flags & ASYNC) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect); effects.push(effect);
} else if (is_dirty(effect)) { } else if (is_dirty(effect)) {
@ -584,6 +584,9 @@ function infinite_loop_guard() {
} }
} }
/** @type {Effect[] | null} */
export let eager_block_effects = null;
/** /**
* @param {Array<Effect>} effects * @param {Array<Effect>} effects
* @returns {void} * @returns {void}
@ -598,7 +601,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++]; var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
var n = current_batch ? current_batch.current.size : 0; eager_block_effects = [];
update_effect(effect); update_effect(effect);
@ -619,21 +622,20 @@ function flush_queued_effects(effects) {
} }
} }
// if state is written in a user effect, abort and re-schedule, lest we run if (eager_block_effects.length > 0) {
// effects that should be removed as a result of the state change // TODO this feels incorrect! it gets the tests passing
if ( old_values.clear();
current_batch !== null &&
current_batch.current.size > n && for (const e of eager_block_effects) {
(effect.f & USER_EFFECT) !== 0 update_effect(e);
) { }
break;
eager_block_effects = [];
} }
} }
} }
while (i < length) { eager_block_effects = null;
schedule_effect(effects[i++]);
}
} }
/** /**

@ -33,7 +33,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js'; import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js'; import { component_context, is_runes } from '../context.js';
import { Batch, schedule_effect } from './batch.js'; import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js'; import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js'; import { execute_derived } from './deriveds.js';
@ -334,6 +334,12 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) { if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else if (not_dirty) { } else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
if (eager_block_effects !== null) {
eager_block_effects.push(/** @type {Effect} */ (reaction));
}
}
schedule_effect(/** @type {Effect} */ (reaction)); schedule_effect(/** @type {Effect} */ (reaction));
} }
} }

@ -15,14 +15,15 @@ const empty = [];
* @template T * @template T
* @param {T} value * @param {T} value
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>} * @returns {Snapshot<T>}
*/ */
export function snapshot(value, skip_warning = false) { export function snapshot(value, skip_warning = false, no_tojson = false) {
if (DEV && !skip_warning) { if (DEV && !skip_warning) {
/** @type {string[]} */ /** @type {string[]} */
const paths = []; const paths = [];
const copy = clone(value, new Map(), '', paths); const copy = clone(value, new Map(), '', paths, null, no_tojson);
if (paths.length === 1 && paths[0] === '') { if (paths.length === 1 && paths[0] === '') {
// value could not be cloned // value could not be cloned
w.state_snapshot_uncloneable(); w.state_snapshot_uncloneable();
@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) {
return copy; return copy;
} }
return clone(value, new Map(), '', empty); return clone(value, new Map(), '', empty, null, no_tojson);
} }
/** /**
@ -49,10 +50,11 @@ export function snapshot(value, skip_warning = false) {
* @param {Map<T, Snapshot<T>>} cloned * @param {Map<T, Snapshot<T>>} cloned
* @param {string} path * @param {string} path
* @param {string[]} paths * @param {string[]} paths
* @param {null | T} original The original value, if `value` was produced from a `toJSON` call * @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>} * @returns {Snapshot<T>}
*/ */
function clone(value, cloned, path, paths, original = null) { function clone(value, cloned, path, paths, original = null, no_tojson = false) {
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
var unwrapped = cloned.get(value); var unwrapped = cloned.get(value);
if (unwrapped !== undefined) return unwrapped; if (unwrapped !== undefined) return unwrapped;
@ -71,7 +73,7 @@ function clone(value, cloned, path, paths, original = null) {
for (var i = 0; i < value.length; i += 1) { for (var i = 0; i < value.length; i += 1) {
var element = value[i]; var element = value[i];
if (i in value) { if (i in value) {
copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths); copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson);
} }
} }
@ -88,8 +90,15 @@ function clone(value, cloned, path, paths, original = null) {
} }
for (var key in value) { for (var key in value) {
// @ts-expect-error copy[key] = clone(
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths); // @ts-expect-error
value[key],
cloned,
DEV ? `${path}.${key}` : path,
paths,
null,
no_tojson
);
} }
return copy; return copy;
@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) {
return /** @type {Snapshot<T>} */ (structuredClone(value)); return /** @type {Snapshot<T>} */ (structuredClone(value));
} }
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') { if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) {
return clone( return clone(
/** @type {T & { toJSON(): any } } */ (value).toJSON(), /** @type {T & { toJSON(): any } } */ (value).toJSON(),
cloned, cloned,

@ -0,0 +1,8 @@
<script>
$effect.pre(() => {
console.log('create A');
return () => console.log('destroy A');
});
</script>
<h1>A</h1>

@ -0,0 +1,8 @@
<script>
$effect.pre(() => {
console.log('create B');
return () => console.log('destroy B');
});
</script>
<h1>B</h1>

@ -0,0 +1,13 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target, logs }) {
const [button] = target.querySelectorAll('button');
flushSync(() => button.click());
assert.deepEqual(logs, ['create A', 'destroy A', 'create B']);
}
});

@ -0,0 +1,13 @@
<script>
import A from './A.svelte';
import B from './B.svelte';
let condition = $state(true);
let Component = $derived(condition ? A : B);
</script>
<button onclick={() => condition = !condition}>
toggle ({condition})
</button>
<Component />

@ -0,0 +1,13 @@
<script>
let { children } = $props();
let inited = $state(false);
$effect(() => {
inited = true;
});
</script>
{#if inited}
<span>{@render children()}</span>
{/if}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');
assert.doesNotThrow(() => {
flushSync(() => button.click());
});
}
});

@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => show = !show}>
toggle
</button>
{#if show}
{#each { length: 1234 } as i}
<Child>{i}</Child>
{/each}
{/if}

@ -1,11 +1,12 @@
<script> <script>
import B from './B.svelte'; import B from './B.svelte';
let { boolean, closed } = $props(); let { boolean, closed, close } = $props();
// this runs after the effect in B, because child effects run first
$effect(() => { $effect(() => {
console.log(boolean); console.log({ boolean, closed });
}); });
</script> </script>
<B {closed} /> <B {closed} {close} />

@ -1,7 +1,5 @@
<script> <script>
import { close } from './Child.svelte'; let { closed, close } = $props();
let { closed } = $props();
$effect(() => { $effect(() => {
if (closed) close(); if (closed) close();

@ -1,20 +0,0 @@
<script module>
let object = $state();
export function open() {
object = { boolean: true };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.boolean}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.boolean)}
{/if}

@ -8,6 +8,6 @@ export default test({
flushSync(() => open.click()); flushSync(() => open.click());
flushSync(() => close.click()); flushSync(() => close.click());
assert.deepEqual(logs, [true]); assert.deepEqual(logs, [{ boolean: true, closed: false }]);
} }
}); });

@ -1,6 +1,15 @@
<script> <script>
import A from './A.svelte'; import A from './A.svelte';
import Child, { open } from './Child.svelte';
let object = $state();
function open() {
object = { boolean: true };
}
function close() {
object = undefined;
}
let closed = $state(false); let closed = $state(false);
</script> </script>
@ -15,9 +24,6 @@
<hr> <hr>
<Child> {#if object}
{#snippet children(boolean)} <A {closed} {close} boolean={object.boolean} />
<A {closed} {boolean} /> {/if}
{/snippet}
</Child>

@ -1,9 +1,9 @@
<script> <script>
import B from './B.svelte'; import B from './B.svelte';
let { boolean, closed } = $props(); let { boolean, closed, close } = $props();
</script> </script>
<span>{boolean}</span> <span>{boolean} {closed}</span>
<B {closed} /> <B {closed} {close} />

@ -1,7 +1,5 @@
<script> <script>
import { close } from './Child.svelte'; let { closed, close } = $props();
let { closed } = $props();
$effect.pre(() => { $effect.pre(() => {
if (closed) close(); if (closed) close();

@ -1,20 +0,0 @@
<script module>
let object = $state();
export function open() {
object = { nested: { boolean: true } };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.nested}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.nested)}
{/if}

@ -1,6 +1,15 @@
<script> <script>
import A from './A.svelte'; import A from './A.svelte';
import Child, { open } from './Child.svelte';
let object = $state();
function open() {
object = { boolean: true };
}
function close() {
object = undefined;
}
let closed = $state(false); let closed = $state(false);
</script> </script>
@ -15,8 +24,6 @@
<hr> <hr>
<Child> {#if object}
{#snippet children(nested)} <A {close} {closed} boolean={object.boolean} />
<A {closed} boolean={nested.boolean} /> {/if}
{/snippet}
</Child>

@ -1,21 +1,15 @@
<script> <script>
class A { class A {
toJSON(){ constructor() {
return { this.a = this;
a: this
}
} }
} }
const state = $state(new A()); const state = $state(new A());
$inspect(state); $inspect(state);
class B { class B {
toJSON(){ constructor() {
return { this.a = { b: this };
a: {
b: this
}
}
} }
} }
const state2 = $state(new B()); const state2 = $state(new B());

@ -0,0 +1,26 @@
import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
dev: true
},
test({ assert, logs }) {
const files = { id: 1, items: [{ id: 2, items: [{ id: 3 }, { id: 4 }] }] };
// @ts-expect-error
files.items[0].parent = files;
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'test (main.svelte:5:4)' },
{ log: '$state', highlighted: true },
{ log: 'filesState.files', highlighted: false },
{ log: files },
{ log: '$state', highlighted: true },
{ log: 'filesState.files.items[0].parent.items', highlighted: false },
{ log: files.items },
{ log: '$state', highlighted: true },
{ log: 'filesState.files.items[0].parent.items[0]', highlighted: false },
{ log: files.items[0] }
]);
}
});

@ -0,0 +1,10 @@
<script>
const filesState = $state({ files: {} });
let nodes = { id: 1, items: [{ id: 2, items: [{ id: 3 }, { id: 4 }] }] };
filesState.files = nodes;
function test() {
$inspect.trace();
filesState.files.items[0].parent = filesState.files;
}
$effect(test);
</script>
Loading…
Cancel
Save