fix: make deriveds on the server lazy again (#15964)

* 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

* fix: handle basic assignment of deriveds on the server

* fix: use `build_assignment_value` for deriveds assignments

* use once

* allow writing to public deriveds on server

---------

Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
pull/15989/head
Simon H 4 months ago committed by GitHub
parent 9250c9cb36
commit ac046df40a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make deriveds on the server lazy again

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

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

@ -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') {

@ -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')))])
);
}
}

@ -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();
}

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

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

@ -5,6 +5,7 @@ export default test({
html: `
<button>0</button>
<p>doubled: 0</p>
<p>tripled: 0</p>
`,
test({ assert, target }) {
@ -17,6 +18,7 @@ export default test({
`
<button>1</button>
<p>doubled: 2</p>
<p>tripled: 3</p>
`
);
@ -27,6 +29,7 @@ export default test({
`
<button>2</button>
<p>doubled: 4</p>
<p>tripled: 6</p>
`
);
}

@ -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);
</script>
<button on:click={() => counter.count++}>{counter.count}</button>
<p>doubled: {counter.embiggened}</p>
<p>doubled: {counter.embiggened1}</p>
<p>tripled: {counter.embiggened2}</p>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `3 3 3 3`
});

@ -0,0 +1,29 @@
<script>
class X {
x = $state(1);
on_class = $derived(this.x * 2);
#on_class_private = $derived(this.x * 2);
#in_constructor_private
constructor() {
this.#in_constructor_private = $derived(this.x * 2);
this.in_constructor = $derived(this.x * 2);
this.#on_class_private = 3;
this.#in_constructor_private = 3;
}
get on_class_private() {
return this.#on_class_private;
}
get in_constructor_private() {
return this.#in_constructor_private;
}
}
const x = new X();
x.on_class = 3;
x.in_constructor = 3;
</script>
{x.on_class} {x.in_constructor} {x.on_class_private} {x.in_constructor_private}
Loading…
Cancel
Save