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/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 280c16dbd26d..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 @@ -44,6 +44,11 @@ function build_assignment(operator, left, right, context) { /** @type {Expression} */ (context.visit(right)) ); } + } else if (field && (field.type === '$derived' || field.type === '$derived.by')) { + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); + return b.call(b.member(b.this, name), value); } } 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 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/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..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 @@ -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('$.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..29e09fe4dd07 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -514,3 +514,24 @@ 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) { + const get_value = once(fn); + /** + * @type {T | undefined} + */ + let updated_value; + + return function (new_value) { + if (arguments.length === 0) { + return updated_value ?? get_value(); + } + 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 141d994a2f85..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,6 +5,7 @@ export default test({ html: `
doubled: 0
+tripled: 0
`, test({ assert, target }) { @@ -17,6 +18,7 @@ export default test({ `doubled: 2
+tripled: 3
` ); @@ -27,6 +29,7 @@ export default test({ `doubled: 4
+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 2c4c8f183970..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,14 +2,24 @@ class Counter { count = $state(0); #doubled = $derived(this.count * 2); + #tripled = $derived.by(() => this.count * this.by); - get embiggened() { + constructor(by) { + this.by = by; + } + + get embiggened1() { return this.#doubled; } + + get embiggened2() { + return this.#tripled; + } } - const counter = new Counter(); + const counter = new Counter(3); -doubled: {counter.embiggened}
+doubled: {counter.embiggened1}
+tripled: {counter.embiggened2}
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}