From 96e5edc6fdf72d920ff449f1f69791fcce5af6e3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 13:05:07 -0400 Subject: [PATCH 1/7] WIP --- packages/svelte/src/internal/client/proxy.js | 58 ++++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4f847f21af66..e016c9967c8a 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -28,47 +28,35 @@ function identity(fn) { return fn; } -/** - * @param {ValueOptions | undefined} options - * @returns {ValueOptions | undefined} - */ -function clone_options(options) { - return options != null - ? { - onchange: options.onchange - } - : undefined; -} - /** @type {ProxyMetadata | null} */ var parent_metadata = null; /** * @template T * @param {T} value - * @param {ValueOptions} [_options] + * @param {ValueOptions} [options] * @param {Source} [prev] dev mode only * @returns {T} */ -export function proxy(value, _options, prev) { +export function proxy(value, options, prev) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null) { return value; } - var options = clone_options(_options); + var onchange = options?.onchange; if (STATE_SYMBOL in value) { // @ts-ignore - value[PROXY_ONCHANGE_SYMBOL](options?.onchange); + value[PROXY_ONCHANGE_SYMBOL](onchange); return value; } - if (options?.onchange) { + if (onchange) { // if there's an onchange we actually store that but override the value // to store every other onchange that new proxies might add - var onchanges = new Set([options.onchange]); - options.onchange = () => { + var onchanges = new Set([onchange]); + onchange = () => { for (let onchange of onchanges) { onchange(); } @@ -118,7 +106,7 @@ export function proxy(value, _options, prev) { // mutations to the array are properly synced with our proxy sources.set( 'length', - source(/** @type {any[]} */ (value).length, clone_options(options), stack) + source(/** @type {any[]} */ (value).length, onchange && { onchange }, stack) ); } @@ -165,7 +153,7 @@ export function proxy(value, _options, prev) { var s = sources.get(prop); if (s === undefined) { - s = with_parent(() => source(descriptor.value, clone_options(options), stack)); + s = with_parent(() => source(descriptor.value, onchange && { onchange }, stack)); sources.set(prop, s); } else { set( @@ -184,7 +172,7 @@ export function proxy(value, _options, prev) { if (prop in target) { sources.set( prop, - with_parent(() => source(UNINITIALIZED, clone_options(options), stack)) + with_parent(() => source(UNINITIALIZED, onchange && { onchange }, stack)) ); } } else { @@ -201,7 +189,7 @@ export function proxy(value, _options, prev) { // when we delete a property if the source is a proxy we remove the current onchange from // the proxy `onchanges` so that it doesn't trigger it anymore if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { - s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); + s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); } set(s, UNINITIALIZED); update_version(version); @@ -227,14 +215,12 @@ export function proxy(value, _options, prev) { // we either add or remove the passed in value // to the onchanges array or we set every source onchange // to the passed in value (if it's undefined it will make the chain stop) - if (options?.onchange != null && value && !remove) { + if (onchange != null && value && !remove) { onchanges?.add?.(value); - } else if (options?.onchange != null && value) { + } else if (onchange != null && value) { onchanges?.delete?.(value); } else { - options = { - onchange: value - }; + onchange = value; for (let [, s] of sources) { if (s.o) { s.o.onchange = value; @@ -249,7 +235,7 @@ export function proxy(value, _options, prev) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { - let opt = clone_options(options); + let opt = onchange && { onchange }; s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED, opt), opt, stack) ); @@ -281,7 +267,7 @@ export function proxy(value, _options, prev) { if ( is_proxied_array && - options?.onchange != null && + onchange != null && array_methods.includes(/** @type {string} */ (prop)) ) { return batch_onchange(v); @@ -330,7 +316,7 @@ export function proxy(value, _options, prev) { (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { - let opt = clone_options(options); + let opt = onchange && { onchange }; s = with_parent(() => source(has ? proxy(target[prop], opt) : UNINITIALIZED, opt, stack)); sources.set(prop, s); } @@ -362,14 +348,14 @@ export function proxy(value, _options, prev) { other_s.v !== null && STATE_SYMBOL in other_s.v ) { - other_s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); + other_s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); } set(other_s, UNINITIALIZED); } else if (i in target) { // If the item exists in the original, we need to create a uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. - other_s = with_parent(() => source(UNINITIALIZED, clone_options(options), stack)); + other_s = with_parent(() => source(UNINITIALIZED, onchange && { onchange }, stack)); sources.set(i + '', other_s); } } @@ -381,7 +367,7 @@ export function proxy(value, _options, prev) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - const opt = clone_options(options); + const opt = onchange && { onchange }; s = with_parent(() => source(undefined, opt, stack)); set( s, @@ -394,11 +380,11 @@ export function proxy(value, _options, prev) { // when we set a property if the source is a proxy we remove the current onchange from // the proxy `onchanges` so that it doesn't trigger it anymore if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { - s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); + s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); } set( s, - with_parent(() => proxy(value, clone_options(options))) + with_parent(() => proxy(value, onchange && { onchange })) ); } })(); From ef1ffbec69add7c0b7e290b7b4884229a0548b60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:24:46 -0400 Subject: [PATCH 2/7] extract onchange callbacks --- .../3-transform/client/visitors/ClassBody.js | 12 ++++-- .../client/visitors/VariableDeclaration.js | 17 +++++---- .../client/visitors/shared/state.js | 35 ++++++++++++++++++ packages/svelte/src/internal/client/index.js | 10 +---- packages/svelte/src/internal/client/proxy.js | 37 ++++++++----------- .../src/internal/client/reactivity/sources.js | 18 +++------ .../src/internal/client/reactivity/types.d.ts | 4 +- 7 files changed, 75 insertions(+), 58 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index e3f8222502ed..c63cf38463fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -6,6 +6,7 @@ import * as b from '../../../../utils/builders.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { get_rune } from '../../../scope.js'; import { should_proxy } from '../utils.js'; +import { get_onchange } from './shared/state.js'; /** * @param {ClassBody} node @@ -117,13 +118,16 @@ export function ClassBody(node, context) { ); if (field.kind === 'state' || field.kind === 'raw_state') { - let arg = definition.value.arguments[1]; - let options = arg && /** @type {Expression} **/ (context.visit(arg, child_state)); + let onchange = get_onchange( + /** @type {Expression} */ (definition.value.arguments[1]), + // @ts-ignore mismatch between Context and ComponentContext. TODO look into + context + ); value = field.kind === 'state' && should_proxy(init, context.state.scope) - ? b.call('$.assignable_proxy', init, options) - : b.call('$.state', init, options); + ? b.call('$.assignable_proxy', init, onchange) + : b.call('$.state', init, onchange); } else { value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 630ee6cef319..d12735d15102 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { is_hoisted_function } from '../../utils.js'; +import { get_onchange } from './shared/state.js'; /** * @param {VariableDeclaration} node @@ -117,26 +118,26 @@ export function VariableDeclaration(node, context) { const args = /** @type {CallExpression} */ (init).arguments; const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; - let options = - args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined; + + const onchange = get_onchange(/** @type {Expression} */ (args[1]), context); if (rune === '$state' || rune === '$state.raw') { /** * @param {Identifier} id * @param {Expression} value - * @param {Expression} [options] + * @param {Expression} [onchange] */ - const create_state_declarator = (id, value, options) => { + const create_state_declarator = (id, value, onchange) => { const binding = /** @type {Binding} */ (context.state.scope.get(id.name)); const proxied = rune === '$state' && should_proxy(value, context.state.scope); const is_state = is_state_source(binding, context.state.analysis); if (proxied) { - return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, options); + return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, onchange); } if (is_state) { - return b.call('$.state', value, options); + return b.call('$.state', value, onchange); } return value; @@ -144,7 +145,7 @@ export function VariableDeclaration(node, context) { if (declarator.id.type === 'Identifier') { declarations.push( - b.declarator(declarator.id, create_state_declarator(declarator.id, value, options)) + b.declarator(declarator.id, create_state_declarator(declarator.id, value, onchange)) ); } else { const tmp = context.state.scope.generate('tmp'); @@ -157,7 +158,7 @@ export function VariableDeclaration(node, context) { return b.declarator( path.node, binding?.kind === 'state' || binding?.kind === 'raw_state' - ? create_state_declarator(binding.node, value, options) + ? create_state_declarator(binding.node, value, onchange) : value ); }) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js new file mode 100644 index 000000000000..9374deb87fe6 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -0,0 +1,35 @@ +/** @import { Expression, Property } from 'estree' */ +/** @import { ComponentContext } from '../../types' */ +import * as b from '../../../../../utils/builders.js'; + +/** + * + * @param {Expression} options + * @param {ComponentContext} context + * @returns {Expression | undefined} + */ +export function get_onchange(options, context) { + if (!options) { + return undefined; + } + + if (options.type === 'ObjectExpression') { + const onchange = /** @type {Property | undefined} */ ( + options.properties.find( + (property) => + property.type === 'Property' && + !property.computed && + property.key.type === 'Identifier' && + property.key.name === 'onchange' + ) + ); + + if (!onchange) { + return undefined; + } + + return /** @type {Expression} */ (context.visit(onchange.value)); + } + + return b.member(/** @type {Expression} */ (context.visit(options)), 'onchange'); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d1293a58705b..a1e7c44370c1 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -113,15 +113,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { - mutable_source, - mutate, - set, - state, - update, - update_pre, - get_options -} from './reactivity/sources.js'; +export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index e016c9967c8a..3f6d5490d3bc 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -34,18 +34,16 @@ var parent_metadata = null; /** * @template T * @param {T} value - * @param {ValueOptions} [options] + * @param {() => void} [onchange] * @param {Source} [prev] dev mode only * @returns {T} */ -export function proxy(value, options, prev) { +export function proxy(value, onchange, prev) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null) { return value; } - var onchange = options?.onchange; - if (STATE_SYMBOL in value) { // @ts-ignore value[PROXY_ONCHANGE_SYMBOL](onchange); @@ -104,10 +102,7 @@ export function proxy(value, options, prev) { if (is_proxied_array) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy - sources.set( - 'length', - source(/** @type {any[]} */ (value).length, onchange && { onchange }, stack) - ); + sources.set('length', source(/** @type {any[]} */ (value).length, onchange, stack)); } /** @type {ProxyMetadata} */ @@ -153,12 +148,12 @@ export function proxy(value, options, prev) { var s = sources.get(prop); if (s === undefined) { - s = with_parent(() => source(descriptor.value, onchange && { onchange }, stack)); + s = with_parent(() => source(descriptor.value, onchange, stack)); sources.set(prop, s); } else { set( s, - with_parent(() => proxy(descriptor.value, options)) + with_parent(() => proxy(descriptor.value, onchange)) ); } @@ -172,7 +167,7 @@ export function proxy(value, options, prev) { if (prop in target) { sources.set( prop, - with_parent(() => source(UNINITIALIZED, onchange && { onchange }, stack)) + with_parent(() => source(UNINITIALIZED, onchange, stack)) ); } } else { @@ -222,9 +217,7 @@ export function proxy(value, options, prev) { } else { onchange = value; for (let [, s] of sources) { - if (s.o) { - s.o.onchange = value; - } + s.o = value; } } }; @@ -235,7 +228,7 @@ export function proxy(value, options, prev) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { - let opt = onchange && { onchange }; + let opt = onchange; s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED, opt), opt, stack) ); @@ -316,7 +309,7 @@ export function proxy(value, options, prev) { (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { - let opt = onchange && { onchange }; + let opt = onchange; s = with_parent(() => source(has ? proxy(target[prop], opt) : UNINITIALIZED, opt, stack)); sources.set(prop, s); } @@ -355,7 +348,7 @@ export function proxy(value, options, prev) { // If the item exists in the original, we need to create a uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. - other_s = with_parent(() => source(UNINITIALIZED, onchange && { onchange }, stack)); + other_s = with_parent(() => source(UNINITIALIZED, onchange, stack)); sources.set(i + '', other_s); } } @@ -367,7 +360,7 @@ export function proxy(value, options, prev) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - const opt = onchange && { onchange }; + const opt = onchange; s = with_parent(() => source(undefined, opt, stack)); set( s, @@ -384,7 +377,7 @@ export function proxy(value, options, prev) { } set( s, - with_parent(() => proxy(value, onchange && { onchange })) + with_parent(() => proxy(value, onchange)) ); } })(); @@ -450,11 +443,11 @@ export function proxy(value, options, prev) { /** * @template T * @param {T} value - * @param {ValueOptions} [options] + * @param {() => void} [onchange] * @returns {Source} */ -export function assignable_proxy(value, options) { - return state(proxy(value, options), options); +export function assignable_proxy(value, onchange) { + return state(proxy(value, onchange), onchange); } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index a20871b3da91..62b8e5dc770d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -77,7 +77,7 @@ export function batch_onchange(fn) { /** * @template V * @param {V} v - * @param {ValueOptions} [o] + * @param {() => void} [o] * @param {Error | null} [stack] * @returns {Source} */ @@ -102,18 +102,10 @@ export function source(v, o, stack) { return signal; } -/** - * @param {Source} source - * @returns {ValueOptions | undefined} - */ -export function get_options(source) { - return source.o; -} - /** * @template V * @param {V} v - * @param {ValueOptions} [o] + * @param {() => void} [o] * @param {Error | null} [stack] */ export function state(v, o, stack) { @@ -196,11 +188,11 @@ export function internal_set(source, value) { if (!source.equals(value)) { var old_value = source.v; - if (typeof old_value === 'object' && old_value != null && source.o?.onchange) { + if (typeof old_value === 'object' && old_value != null && source.o) { // @ts-ignore const remove = old_value[PROXY_ONCHANGE_SYMBOL]; if (remove && typeof remove === 'function') { - remove(source.o?.onchange, true); + remove(source.o, true); } } @@ -257,7 +249,7 @@ export function internal_set(source, value) { inspect_effects.clear(); } - var onchange = source.o?.onchange; + var onchange = source.o; if (onchange) { if (onchange_batch) { onchange_batch.add(onchange); diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index c0d490d5c7ab..c43363a9b146 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -20,8 +20,8 @@ export interface Value extends Signal { rv: number; /** The latest value for this signal */ v: V; - /** Options for the source */ - o?: ValueOptions; + /** onchange callback */ + o?: () => void; /** Dev only */ created?: Error | null; updated?: Error | null; From 8fe6f32c1fd9c5a306943f7fac7209df476e1d52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:28:35 -0400 Subject: [PATCH 3/7] const --- .../compiler/phases/3-transform/client/visitors/ClassBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index c63cf38463fe..b67bb8893c30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -118,7 +118,7 @@ export function ClassBody(node, context) { ); if (field.kind === 'state' || field.kind === 'raw_state') { - let onchange = get_onchange( + const onchange = get_onchange( /** @type {Expression} */ (definition.value.arguments[1]), // @ts-ignore mismatch between Context and ComponentContext. TODO look into context From f0732a83ed2d78953370efd1499edf46427b550f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:29:43 -0400 Subject: [PATCH 4/7] tweak --- .../phases/3-transform/client/visitors/shared/state.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js index 9374deb87fe6..be7f2cabdab9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -9,9 +9,7 @@ import * as b from '../../../../../utils/builders.js'; * @returns {Expression | undefined} */ export function get_onchange(options, context) { - if (!options) { - return undefined; - } + if (!options) return; if (options.type === 'ObjectExpression') { const onchange = /** @type {Property | undefined} */ ( @@ -24,9 +22,7 @@ export function get_onchange(options, context) { ) ); - if (!onchange) { - return undefined; - } + if (!onchange) return; return /** @type {Expression} */ (context.visit(onchange.value)); } From c77dfde5e92d014a0be1e22e2299f98da2c90aea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:30:27 -0400 Subject: [PATCH 5/7] docs --- .../compiler/phases/3-transform/client/visitors/shared/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js index be7f2cabdab9..0f8a7b1b5bfd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -3,7 +3,7 @@ import * as b from '../../../../../utils/builders.js'; /** - * + * Extract the `onchange` callback from the options passed to `$state` * @param {Expression} options * @param {ComponentContext} context * @returns {Expression | undefined} From af3c5f465149126315527857396e8cbcc88b77ea Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Fri, 21 Mar 2025 22:45:44 +0100 Subject: [PATCH 6/7] fix: unwrap args in case of spread --- .../3-transform/client/visitors/ClassBody.js | 30 ++++++++++++++----- .../client/visitors/VariableDeclaration.js | 19 +++++++++--- .../client/visitors/shared/state.js | 8 +++-- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index b67bb8893c30..67104b4f8959 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -1,4 +1,4 @@ -/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */ +/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, SpreadElement } from 'estree' */ /** @import { } from '#compiler' */ /** @import { Context, StateField } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; @@ -113,23 +113,37 @@ export function ClassBody(node, context) { let value = null; if (definition.value.arguments.length > 0) { - const init = /** @type {Expression} **/ ( + let init = /** @type {Expression | SpreadElement} **/ ( context.visit(definition.value.arguments[0], child_state) ); if (field.kind === 'state' || field.kind === 'raw_state') { - const onchange = get_onchange( - /** @type {Expression} */ (definition.value.arguments[1]), - // @ts-ignore mismatch between Context and ComponentContext. TODO look into - context - ); + let onchange; + if ( + /** @type {Expression | SpreadElement} */ (/** @type {unknown} */ (init)).type === + 'SpreadElement' + ) { + const argument = init.argument; + init = b.member(argument, '0', true); + onchange = b.member(b.member(argument, '1', true), 'onchange', false, true); + } + if (!onchange) { + onchange = get_onchange( + /** @type {Expression} */ (definition.value.arguments[1]), + // @ts-ignore mismatch between Context and ComponentContext. TODO look into + context + ); + } value = field.kind === 'state' && should_proxy(init, context.state.scope) ? b.call('$.assignable_proxy', init, onchange) : b.call('$.state', init, onchange); } else { - value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init)); + value = b.call( + '$.derived', + field.kind === 'derived_by' ? init : b.thunk(/** @type {Expression} */ (init)) + ); } } else { // if no arguments, we know it's state as `$derived()` is a compile error diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index d12735d15102..eb019c57039c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator, SpreadElement } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -117,9 +117,20 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; - - const onchange = get_onchange(/** @type {Expression} */ (args[1]), context); + let value = + args.length > 0 + ? /** @type {Expression | SpreadElement} */ (context.visit(args[0])) + : b.void0; + let onchange; + + if (value.type === 'SpreadElement') { + const argument = value.argument; + value = b.member(argument, '0', true); + onchange = b.member(b.member(argument, '1', true), 'onchange', false, true); + } + if (!onchange) { + onchange = get_onchange(/** @type {Expression} */ (args[1]), context); + } if (rune === '$state' || rune === '$state.raw') { /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js index 0f8a7b1b5bfd..c3542f16c1d9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -1,10 +1,10 @@ -/** @import { Expression, Property } from 'estree' */ +/** @import { Expression, Property, SpreadElement } from 'estree' */ /** @import { ComponentContext } from '../../types' */ import * as b from '../../../../../utils/builders.js'; /** * Extract the `onchange` callback from the options passed to `$state` - * @param {Expression} options + * @param {Expression | SpreadElement} options * @param {ComponentContext} context * @returns {Expression | undefined} */ @@ -27,5 +27,9 @@ export function get_onchange(options, context) { return /** @type {Expression} */ (context.visit(onchange.value)); } + if (options.type === 'SpreadElement') { + return b.member(b.member(options.argument, '0', true), 'onchange'); + } + return b.member(/** @type {Expression} */ (context.visit(options)), 'onchange'); } From e4c799210bc1de227ed6f18ee7fffaf7f16b697c Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Sat, 22 Mar 2025 00:07:04 +0100 Subject: [PATCH 7/7] fix: revert unwrap args in case of spread --- .../3-transform/client/visitors/ClassBody.js | 30 +++++-------------- .../client/visitors/VariableDeclaration.js | 19 +++--------- .../client/visitors/shared/state.js | 8 ++--- 3 files changed, 14 insertions(+), 43 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 67104b4f8959..b67bb8893c30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -1,4 +1,4 @@ -/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, SpreadElement } from 'estree' */ +/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */ /** @import { } from '#compiler' */ /** @import { Context, StateField } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; @@ -113,37 +113,23 @@ export function ClassBody(node, context) { let value = null; if (definition.value.arguments.length > 0) { - let init = /** @type {Expression | SpreadElement} **/ ( + const init = /** @type {Expression} **/ ( context.visit(definition.value.arguments[0], child_state) ); if (field.kind === 'state' || field.kind === 'raw_state') { - let onchange; - if ( - /** @type {Expression | SpreadElement} */ (/** @type {unknown} */ (init)).type === - 'SpreadElement' - ) { - const argument = init.argument; - init = b.member(argument, '0', true); - onchange = b.member(b.member(argument, '1', true), 'onchange', false, true); - } - if (!onchange) { - onchange = get_onchange( - /** @type {Expression} */ (definition.value.arguments[1]), - // @ts-ignore mismatch between Context and ComponentContext. TODO look into - context - ); - } + const onchange = get_onchange( + /** @type {Expression} */ (definition.value.arguments[1]), + // @ts-ignore mismatch between Context and ComponentContext. TODO look into + context + ); value = field.kind === 'state' && should_proxy(init, context.state.scope) ? b.call('$.assignable_proxy', init, onchange) : b.call('$.state', init, onchange); } else { - value = b.call( - '$.derived', - field.kind === 'derived_by' ? init : b.thunk(/** @type {Expression} */ (init)) - ); + value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init)); } } else { // if no arguments, we know it's state as `$derived()` is a compile error diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index eb019c57039c..d12735d15102 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator, SpreadElement } from 'estree' */ +/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -117,20 +117,9 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - let value = - args.length > 0 - ? /** @type {Expression | SpreadElement} */ (context.visit(args[0])) - : b.void0; - let onchange; - - if (value.type === 'SpreadElement') { - const argument = value.argument; - value = b.member(argument, '0', true); - onchange = b.member(b.member(argument, '1', true), 'onchange', false, true); - } - if (!onchange) { - onchange = get_onchange(/** @type {Expression} */ (args[1]), context); - } + const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; + + const onchange = get_onchange(/** @type {Expression} */ (args[1]), context); if (rune === '$state' || rune === '$state.raw') { /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js index c3542f16c1d9..0f8a7b1b5bfd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -1,10 +1,10 @@ -/** @import { Expression, Property, SpreadElement } from 'estree' */ +/** @import { Expression, Property } from 'estree' */ /** @import { ComponentContext } from '../../types' */ import * as b from '../../../../../utils/builders.js'; /** * Extract the `onchange` callback from the options passed to `$state` - * @param {Expression | SpreadElement} options + * @param {Expression} options * @param {ComponentContext} context * @returns {Expression | undefined} */ @@ -27,9 +27,5 @@ export function get_onchange(options, context) { return /** @type {Expression} */ (context.visit(onchange.value)); } - if (options.type === 'SpreadElement') { - return b.member(b.member(options.argument, '0', true), 'onchange'); - } - return b.member(/** @type {Expression} */ (context.visit(options)), 'onchange'); }