fix: improve derived output for ssr (#10757)

* fix: improve derived output for ssr

* ts

* Update .changeset/rotten-rules-invite.md

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
pull/10758/head
Dominic Gannaway 2 years ago committed by GitHub
parent 6f3fcc0696
commit ef206fe1b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use getters for derived class state fields, with memoisation

@ -17,7 +17,6 @@ export const global_visitors = {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name);
if (field) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
}

@ -546,82 +546,112 @@ const javascript_visitors = {
/** @type {import('./types').Visitors} */
const javascript_visitors_runes = {
ClassBody(node, { state, visit, next }) {
if (!state.analysis.runes) {
next();
}
/** @type {import('estree').PropertyDefinition[]} */
const deriveds = [];
/** @type {import('estree').MethodDefinition | null} */
let constructor = null;
// Get the constructor
for (const definition of node.body) {
if (definition.type === 'MethodDefinition' && definition.kind === 'constructor') {
constructor = /** @type {import('estree').MethodDefinition} */ (visit(definition));
}
}
// Move $derived() runes to the end of the body if there is a constructor
if (constructor !== null) {
const body = [];
ClassBody(node, { state, visit }) {
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const public_derived = new Map();
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const private_derived = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const is_private = definition.key.type === 'PrivateIdentifier';
const { type, name } = definition.key;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$derived' || rune === '$derived.by') {
/** @type {import('../../3-transform/client/types.js').StateField} */
const field = {
kind: rune === '$derived.by' ? 'derived_call' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (rune === '$derived') {
deriveds.push(/** @type {import('estree').PropertyDefinition} */ (visit(definition)));
if (is_private) {
// Keep the private #name initializer if private, but remove initial value
body.push({
...definition,
value: null
});
private_derived.set(name, field);
} else {
public_derived.set(name, field);
}
}
}
}
continue;
}
// each `foo = $derived()` needs a backing `#foo` field
for (const [name, field] of public_derived) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
if (definition.type !== 'MethodDefinition' || definition.kind !== 'constructor') {
/** @type {Array<import('estree').MethodDefinition | import('estree').PropertyDefinition>} */
const body = [];
const child_state = { ...state, private_derived };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const name = definition.key.name;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_derived : public_derived).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
const init = /** @type {import('estree').Expression} **/ (
visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_call'
? b.call('$.once', init)
: b.call('$.once', b.thunk(init));
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
if ((field.kind === 'derived' || field.kind === 'derived_call') && state.options.dev) {
body.push(
/** @type {import('estree').PropertyDefinition | import('estree').MethodDefinition | import('estree').StaticBlock} */ (
visit(definition)
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
if (deriveds.length > 0) {
body.push({
...constructor,
value: {
...constructor.value,
body: b.block([
...constructor.value.body.body,
...deriveds.map((d) => {
return b.stmt(
b.assignment(
'=',
b.member(b.this, d.key),
/** @type {import('estree').Expression} */ (d.value)
)
);
})
])
continue;
}
});
} else {
body.push(constructor);
}
return {
...node,
body
};
body.push(/** @type {import('estree').MethodDefinition} **/ (visit(definition, child_state)));
}
next();
return { ...node, body };
},
PropertyDefinition(node, { state, next, visit }) {
if (node.value != null && node.value.type === 'CallExpression') {
@ -730,6 +760,16 @@ const javascript_visitors_runes = {
return transform_inspect_rune(node, context);
}
context.next();
},
MemberExpression(node, context) {
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
const field = context.state.private_derived.get(node.property.name);
if (field) {
return b.call(node);
}
}
context.next();
}
};
@ -2089,7 +2129,8 @@ export function server_component(analysis, options) {
metadata: {
namespace: options.namespace
},
preserve_whitespace: options.preserveWhitespace
preserve_whitespace: options.preserveWhitespace,
private_derived: new Map()
};
const module = /** @type {import('estree').Program} */ (
@ -2346,7 +2387,8 @@ export function server_module(analysis, options) {
// this is an anomaly — it can only be used in components, but it needs
// to be present for `javascript_visitors` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map()
legacy_reactive_statements: new Map(),
private_derived: new Map()
};
const module = /** @type {import('estree').Program} */ (

@ -8,6 +8,7 @@ import type {
import type { SvelteNode, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js';
export type TemplateExpression = {
type: 'expression';
@ -35,6 +36,7 @@ export interface Anchor {
export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
readonly private_derived: Map<string, StateField>;
}
export interface ComponentServerTransformState extends ServerTransformState {

@ -8,6 +8,7 @@ import {
is_tag_valid_with_parent
} from '../../constants.js';
import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../client/constants.js';
export * from '../client/validate.js';
@ -666,3 +667,17 @@ export function loop_guard(timeout) {
export function inspect(args, inspect = console.log) {
inspect('init', ...args);
}
/**
* @template V
* @param {() => V} get_value
*/
export function once(get_value) {
let value = /** @type {V} */ (UNINITIALIZED);
return () => {
if (value === UNINITIALIZED) {
value = get_value();
}
return value;
};
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>a\n1</p><p>b\n2</p><p>c\n4</p>`
});

@ -0,0 +1,17 @@
<script>
class Foo {
a = $state(0);
c = $derived(this.b * 2);
b = $derived(this.a * 2);
constructor(a) {
this.a = a;
}
}
const foo = new Foo(1);
</script>
<p>a {foo.a}</p>
<p>b {foo.b}</p>
<p>c {foo.c}</p>
Loading…
Cancel
Save