From e6c4e8c5c513c06e1f244c343ce1a9c7d4999748 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 20 May 2025 13:09:54 +0200 Subject: [PATCH 1/5] fix: make deriveds on the server lazy again Fixes a regression introduced in #15820: deriveds need to be lazily called on the server, too, since they can close over variables only later defined Fixes #15960 --- .changeset/khaki-carrots-destroy.md | 5 ++++ .../3-transform/server/transform-server.js | 2 ++ .../3-transform/server/visitors/ClassBody.js | 4 ---- .../server/visitors/MemberExpression.js | 23 +++++++++++++++++++ .../server/visitors/PropertyDefinition.js | 7 +++--- .../class-state-derived-private/_config.js | 3 +++ .../class-state-derived-private/main.svelte | 16 ++++++++++--- 7 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 .changeset/khaki-carrots-destroy.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js diff --git a/.changeset/khaki-carrots-destroy.md b/.changeset/khaki-carrots-destroy.md new file mode 100644 index 000000000000..677a83643de0 --- /dev/null +++ b/.changeset/khaki-carrots-destroy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make deriveds on the server lazy again diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 3498b2636b15..7a3d6bef6c31 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -23,6 +23,7 @@ import { Identifier } from './visitors/Identifier.js'; import { IfBlock } from './visitors/IfBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; +import { MemberExpression } from './visitors/MemberExpression.js'; import { PropertyDefinition } from './visitors/PropertyDefinition.js'; import { RegularElement } from './visitors/RegularElement.js'; import { RenderTag } from './visitors/RenderTag.js'; @@ -48,6 +49,7 @@ const global_visitors = { ExpressionStatement, Identifier, LabeledStatement, + MemberExpression, PropertyDefinition, UpdateExpression, VariableDeclaration diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 6797b0beffd2..0f65375670ec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -22,10 +22,6 @@ export function ClassBody(node, context) { const child_state = { ...context.state, state_fields }; for (const [name, field] of state_fields) { - if (name[0] === '#') { - continue; - } - // insert backing fields for stuff declared in the constructor if ( field && diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js new file mode 100644 index 000000000000..50b5ae793fb4 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js @@ -0,0 +1,23 @@ +/** @import { ClassBody, MemberExpression } from 'estree' */ +/** @import { Context } from '../types.js' */ +import * as b from '#compiler/builders'; + +/** + * @param {MemberExpression} node + * @param {Context} context + */ +export function MemberExpression(node, context) { + if ( + context.state.analysis.runes && + node.object.type === 'ThisExpression' && + node.property.type === 'PrivateIdentifier' + ) { + const field = context.state.state_fields?.get(`#${node.property.name}`); + + if (field?.type === '$derived' || field?.type === '$derived.by') { + return b.call(node); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js index c9225bb8da6f..d83dc783b67b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js @@ -11,7 +11,7 @@ export function PropertyDefinition(node, context) { if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') { const rune = get_rune(node.value, context.state.scope); - if (rune === '$state' || rune === '$state.raw' || rune === '$derived') { + if (rune === '$state' || rune === '$state.raw') { return { ...node, value: @@ -21,13 +21,14 @@ export function PropertyDefinition(node, context) { }; } - if (rune === '$derived.by') { + if (rune === '$derived.by' || rune === '$derived') { + const fn = /** @type {Expression} */ (context.visit(node.value.arguments[0])); return { ...node, value: node.value.arguments.length === 0 ? null - : b.call(/** @type {Expression} */ (context.visit(node.value.arguments[0]))) + : b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn) }; } } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js index 141d994a2f85..a6b42605ed3d 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js @@ -5,6 +5,7 @@ export default test({ html: `

doubled: 0

+

trippled: 0

`, test({ assert, target }) { @@ -17,6 +18,7 @@ export default test({ `

doubled: 2

+

trippled: 3

` ); @@ -27,6 +29,7 @@ export default test({ `

doubled: 4

+

trippled: 9

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte index 2c4c8f183970..5188dfd8bacc 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte @@ -2,14 +2,24 @@ class Counter { count = $state(0); #doubled = $derived(this.count * 2); + #trippled = $derived.by(() => this.count * this.by); - get embiggened() { + constructor(by) { + this.by = by; + } + + get embiggened1() { return this.#doubled; } + + get embiggened2() { + return this.#trippled; + } } - const counter = new Counter(); + const counter = new Counter(3); -

doubled: {counter.embiggened}

+

doubled: {counter.embiggened1}

+

trippled: {counter.embiggened2}

From 69bd060c9604c0632d08cf322433186618be15a5 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 20 May 2025 22:20:17 +0200 Subject: [PATCH 2/5] fix: handle basic assignment of deriveds on the server --- .../server/visitors/AssignmentExpression.js | 2 ++ .../server/visitors/CallExpression.js | 2 +- .../3-transform/server/visitors/ClassBody.js | 4 ++++ .../server/visitors/PropertyDefinition.js | 2 +- packages/svelte/src/internal/server/index.js | 20 +++++++++++++++++++ .../class-state-derived-private/_config.js | 6 +++--- .../class-state-derived-private/main.svelte | 6 +++--- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 280c16dbd26d..b4d738eaa170 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -44,6 +44,8 @@ function build_assignment(operator, left, right, context) { /** @type {Expression} */ (context.visit(right)) ); } + } else if (field && (field.type === '$derived' || field.type === '$derived.by')) { + return b.call(b.member(b.this, name), right); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index e36dc820b3ef..35c79988b08b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -31,7 +31,7 @@ export function CallExpression(node, context) { if (rune === '$derived' || rune === '$derived.by') { const fn = /** @type {Expression} */ (context.visit(node.arguments[0])); - return b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn); + return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); } if (rune === '$state.snapshot') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 0f65375670ec..6797b0beffd2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -22,6 +22,10 @@ export function ClassBody(node, context) { const child_state = { ...context.state, state_fields }; for (const [name, field] of state_fields) { + if (name[0] === '#') { + continue; + } + // insert backing fields for stuff declared in the constructor if ( field && diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js index d83dc783b67b..498f90703c7b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js @@ -28,7 +28,7 @@ export function PropertyDefinition(node, context) { value: node.value.arguments.length === 0 ? null - : b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn) + : b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn) }; } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b58a1d4372a6..fa3f9a0f28c8 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -514,3 +514,23 @@ export { } from '../shared/validate.js'; export { escape_html as escape }; + +/** + * @template T + * @param {()=>T} fn + * @returns {(new_value?: T) => (T | void)} + */ +export function derived(fn) { + /** + * @type {T | undefined} + */ + let updated_value; + + return function (new_value) { + if (arguments.length === 0) { + return updated_value ?? fn(); + } + updated_value = new_value; + return updated_value; + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js index a6b42605ed3d..40ef84a2e6f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js @@ -5,7 +5,7 @@ export default test({ html: `

doubled: 0

-

trippled: 0

+

tripled: 0

`, test({ assert, target }) { @@ -18,7 +18,7 @@ export default test({ `

doubled: 2

-

trippled: 3

+

tripled: 3

` ); @@ -29,7 +29,7 @@ export default test({ `

doubled: 4

-

trippled: 9

+

tripled: 6

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte index 5188dfd8bacc..d971566396d6 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte @@ -2,7 +2,7 @@ class Counter { count = $state(0); #doubled = $derived(this.count * 2); - #trippled = $derived.by(() => this.count * this.by); + #tripled = $derived.by(() => this.count * this.by); constructor(by) { this.by = by; @@ -13,7 +13,7 @@ } get embiggened2() { - return this.#trippled; + return this.#tripled; } } @@ -22,4 +22,4 @@

doubled: {counter.embiggened1}

-

trippled: {counter.embiggened2}

+

tripled: {counter.embiggened2}

From dadff1650015f6704646003c7bbdf9cf6fdf1480 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 21 May 2025 12:01:50 +0200 Subject: [PATCH 3/5] fix: use `build_assignment_value` for deriveds assignments --- .../3-transform/server/visitors/AssignmentExpression.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index b4d738eaa170..466682fb82b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -45,7 +45,10 @@ function build_assignment(operator, left, right, context) { ); } } else if (field && (field.type === '$derived' || field.type === '$derived.by')) { - return b.call(b.member(b.this, name), right); + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); + return b.call(b.member(b.this, name), value); } } From 895e0481b484d62ac34878942d95376cbd70d277 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 22 May 2025 21:55:34 +0200 Subject: [PATCH 4/5] use once --- packages/svelte/src/internal/server/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index fa3f9a0f28c8..29e09fe4dd07 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -521,6 +521,7 @@ export { escape_html as escape }; * @returns {(new_value?: T) => (T | void)} */ export function derived(fn) { + const get_value = once(fn); /** * @type {T | undefined} */ @@ -528,7 +529,7 @@ export function derived(fn) { return function (new_value) { if (arguments.length === 0) { - return updated_value ?? fn(); + return updated_value ?? get_value(); } updated_value = new_value; return updated_value; From 5ba35727d274aa24ef14a4edeef6cf332ef6f2e5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 22 May 2025 22:40:20 +0200 Subject: [PATCH 5/5] allow writing to public deriveds on server --- .../3-transform/server/visitors/ClassBody.js | 7 +++-- .../samples/writable-derived-3/_config.js | 5 ++++ .../samples/writable-derived-3/main.svelte | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 6797b0beffd2..432d0142cdbf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -36,7 +36,8 @@ export function ClassBody(node, context) { body.push( b.prop_def(field.key, null), - b.method('get', b.key(name), [], [b.return(b.call(member))]) + b.method('get', b.key(name), [], [b.return(b.call(member))]), + b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))]) ); } } @@ -61,6 +62,7 @@ export function ClassBody(node, context) { if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); } else if (field.node === definition) { + // $derived / $derived.by const member = b.member(b.this, field.key); body.push( @@ -69,7 +71,8 @@ export function ClassBody(node, context) { /** @type {CallExpression} */ (context.visit(field.value, child_state)) ), - b.method('get', definition.key, [], [b.return(b.call(member))]) + b.method('get', definition.key, [], [b.return(b.call(member))]), + b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))]) ); } } diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js new file mode 100644 index 000000000000..999e4ad6e05c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `3 3 3 3` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte new file mode 100644 index 000000000000..0b20f811c338 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte @@ -0,0 +1,29 @@ + + +{x.on_class} {x.in_constructor} {x.on_class_private} {x.in_constructor_private}