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;
}
var snap = snapshot(value, true);
var snap = snapshot(value, true, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
});

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

@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
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
/** @type {Window} */
@ -214,6 +215,7 @@ export function clear_text_content(node) {
*/
export function should_defer_append() {
if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;

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

@ -292,12 +292,12 @@ export class Batch {
if (!skip && effect.fn !== null) {
if (is_branch) {
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) {
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 & ASYNC) !== 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(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
* @returns {void}
@ -598,7 +601,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
var n = current_batch ? current_batch.current.size : 0;
eager_block_effects = [];
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
// effects that should be removed as a result of the state change
if (
current_batch !== null &&
current_batch.current.size > n &&
(effect.f & USER_EFFECT) !== 0
) {
break;
if (eager_block_effects.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
for (const e of eager_block_effects) {
update_effect(e);
}
eager_block_effects = [];
}
}
}
while (i < length) {
schedule_effect(effects[i++]);
}
eager_block_effects = null;
}
/**

@ -33,7 +33,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.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 { execute_derived } from './deriveds.js';
@ -334,6 +334,12 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_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));
}
}

@ -15,14 +15,15 @@ const empty = [];
* @template T
* @param {T} value
* @param {boolean} [skip_warning]
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>}
*/
export function snapshot(value, skip_warning = false) {
export function snapshot(value, skip_warning = false, no_tojson = false) {
if (DEV && !skip_warning) {
/** @type {string[]} */
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] === '') {
// value could not be cloned
w.state_snapshot_uncloneable();
@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) {
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 {string} path
* @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>}
*/
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) {
var unwrapped = cloned.get(value);
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) {
var element = value[i];
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) {
// @ts-expect-error
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
copy[key] = clone(
// @ts-expect-error
value[key],
cloned,
DEV ? `${path}.${key}` : path,
paths,
null,
no_tojson
);
}
return copy;
@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) {
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(
/** @type {T & { toJSON(): any } } */ (value).toJSON(),
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>
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(() => {
console.log(boolean);
console.log({ boolean, closed });
});
</script>
<B {closed} />
<B {closed} {close} />

@ -1,7 +1,5 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
let { closed, close } = $props();
$effect(() => {
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(() => close.click());
assert.deepEqual(logs, [true]);
assert.deepEqual(logs, [{ boolean: true, closed: false }]);
}
});

@ -1,6 +1,15 @@
<script>
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);
</script>
@ -15,9 +24,6 @@
<hr>
<Child>
{#snippet children(boolean)}
<A {closed} {boolean} />
{/snippet}
</Child>
{#if object}
<A {closed} {close} boolean={object.boolean} />
{/if}

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

@ -1,7 +1,5 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
let { closed, close } = $props();
$effect.pre(() => {
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>
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);
</script>
@ -15,8 +24,6 @@
<hr>
<Child>
{#snippet children(nested)}
<A {closed} boolean={nested.boolean} />
{/snippet}
</Child>
{#if object}
<A {close} {closed} boolean={object.boolean} />
{/if}

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