Merge branch 'gh-16548' into gh-16548-eager-block

pull/16631/head
Rich Harris 3 weeks ago
commit 8e2cc027a3

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

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

@ -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) {
copy[key] = clone(
// @ts-expect-error
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths);
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 />

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