From b2b3399419e42dd1646db1d28bfc8f8526e29f9c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 9 Oct 2024 11:01:11 +0100 Subject: [PATCH 1/4] breaking: array proxy toPrimitive is no longer reactive --- .changeset/gorgeous-jokes-sit.md | 5 ++++ packages/svelte/src/internal/client/proxy.js | 8 ++++- .../samples/inspect-derived-2/main.svelte | 2 +- .../props-default-value-lazy/sub.svelte | 2 +- .../samples/proxy-to-primitive/_config.js | 30 +++++++++++++++++++ .../samples/proxy-to-primitive/main.svelte | 21 +++++++++++++ 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 .changeset/gorgeous-jokes-sit.md create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte diff --git a/.changeset/gorgeous-jokes-sit.md b/.changeset/gorgeous-jokes-sit.md new file mode 100644 index 000000000000..09484f713103 --- /dev/null +++ b/.changeset/gorgeous-jokes-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: array proxy toPrimitive is no longer reactive diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 7454a22183ac..915b88c56f8e 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,6 @@ /** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, component_context, active_effect } from './runtime.js'; +import { get, component_context, active_effect, untrack } from './runtime.js'; import { array_prototype, get_descriptor, @@ -115,6 +115,12 @@ export function proxy(value, parent = null, prev) { if (DEV && prop === STATE_SYMBOL_METADATA) { return metadata; } + // We untrack Symbol.toPrimitive cases. If people want explicit reactivity, they should + // use toString() or some other coercion method instead + if (is_proxied_array && prop === Symbol.toPrimitive) { + return (/** @type {'string' | 'number' | 'default'} */ hint) => + untrack(() => (hint === 'number' ? Number(target) : String(target))); + } if (prop === STATE_SYMBOL) { return value; diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte index 1071d37c39c6..14e9e7acc75f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte @@ -18,4 +18,4 @@ -{state.data.list} +{state.data.list.toString()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js new file mode 100644 index 000000000000..684e5870c19d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + `, + + ssrHtml: ` + + `, + + test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte new file mode 100644 index 000000000000..a02ab881c6e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte @@ -0,0 +1,21 @@ + + + + + From 7cfff11f1e86048c7c114c11f300b8bfb856a4c6 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 9 Oct 2024 11:07:51 +0100 Subject: [PATCH 2/4] test --- .../samples/props-default-value-lazy-accessors/main.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

From 7a3002c007373182c27d7abd507cc022493d801f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 9 Oct 2024 11:16:16 +0100 Subject: [PATCH 3/4] update --- .changeset/gorgeous-jokes-sit.md | 2 +- .../svelte/src/internal/client/constants.js | 29 ++++++++++--------- packages/svelte/src/internal/client/proxy.js | 13 ++++++--- .../src/internal/client/reactivity/effects.js | 5 ++-- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.changeset/gorgeous-jokes-sit.md b/.changeset/gorgeous-jokes-sit.md index 09484f713103..934bae780fce 100644 --- a/.changeset/gorgeous-jokes-sit.md +++ b/.changeset/gorgeous-jokes-sit.md @@ -2,4 +2,4 @@ 'svelte': patch --- -breaking: array proxy toPrimitive is no longer reactive +breaking: array proxy coercion is no longer reactive in template diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 53df86126ac4..dd9fc2a5249e 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -3,22 +3,23 @@ export const EFFECT = 1 << 2; export const RENDER_EFFECT = 1 << 3; export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; -export const ROOT_EFFECT = 1 << 6; -export const UNOWNED = 1 << 7; -export const DISCONNECTED = 1 << 8; -export const CLEAN = 1 << 9; -export const DIRTY = 1 << 10; -export const MAYBE_DIRTY = 1 << 11; -export const INERT = 1 << 12; -export const DESTROYED = 1 << 13; -export const EFFECT_RAN = 1 << 14; +export const TEMPLATE_EFFECT = 1 << 6; +export const ROOT_EFFECT = 1 << 7; +export const UNOWNED = 1 << 8; +export const DISCONNECTED = 1 << 9; +export const CLEAN = 1 << 10; +export const DIRTY = 1 << 11; +export const MAYBE_DIRTY = 1 << 12; +export const INERT = 1 << 13; +export const DESTROYED = 1 << 14; +export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 15; +export const EFFECT_TRANSPARENT = 1 << 16; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 16; -export const INSPECT_EFFECT = 1 << 17; -export const HEAD_EFFECT = 1 << 18; -export const EFFECT_HAS_DERIVED = 1 << 19; +export const LEGACY_DERIVED_PROP = 1 << 17; +export const INSPECT_EFFECT = 1 << 18; +export const HEAD_EFFECT = 1 << 19; +export const EFFECT_HAS_DERIVED = 1 << 20; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 915b88c56f8e..2ea7b335b55c 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -10,7 +10,7 @@ import { } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; import { source, set } from './reactivity/sources.js'; -import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; +import { STATE_SYMBOL, STATE_SYMBOL_METADATA, TEMPLATE_EFFECT } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -115,9 +115,14 @@ export function proxy(value, parent = null, prev) { if (DEV && prop === STATE_SYMBOL_METADATA) { return metadata; } - // We untrack Symbol.toPrimitive cases. If people want explicit reactivity, they should - // use toString() or some other coercion method instead - if (is_proxied_array && prop === Symbol.toPrimitive) { + // We untrack Symbol.toPrimitive when used within a template effect. If people want explicit reactivity, + // they should use toString() or some other coercion method instead + if ( + is_proxied_array && + prop === Symbol.toPrimitive && + active_effect !== null && + (active_effect.f & TEMPLATE_EFFECT) !== 0 + ) { return (/** @type {'string' | 'number' | 'default'} */ hint) => untrack(() => (hint === 'number' ? Number(target) : String(target))); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b5955e9b32c0..41dd61c18070 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -35,7 +35,8 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_HAS_DERIVED + EFFECT_HAS_DERIVED, + TEMPLATE_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -324,7 +325,7 @@ export function template_effect(fn) { value: '{expression}' }); } - return render_effect(fn); + return create_effect(RENDER_EFFECT | TEMPLATE_EFFECT, fn, true); } /** From 3d983fba7d3e2171048be748bded69a046251cca Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 9 Oct 2024 11:39:31 +0100 Subject: [PATCH 4/4] tweak --- packages/svelte/src/internal/client/proxy.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 2ea7b335b55c..3409b013cc5f 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,6 @@ /** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, component_context, active_effect, untrack } from './runtime.js'; +import { get, component_context, active_effect, untrack, active_reaction } from './runtime.js'; import { array_prototype, get_descriptor, @@ -120,8 +120,8 @@ export function proxy(value, parent = null, prev) { if ( is_proxied_array && prop === Symbol.toPrimitive && - active_effect !== null && - (active_effect.f & TEMPLATE_EFFECT) !== 0 + active_reaction !== null && + (active_reaction.f & TEMPLATE_EFFECT) !== 0 ) { return (/** @type {'string' | 'number' | 'default'} */ hint) => untrack(() => (hint === 'number' ? Number(target) : String(target)));