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 // rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') { if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name); const field = state.private_state.get(node.property.name);
if (field) { if (field) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node); 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} */ /** @type {import('./types').Visitors} */
const javascript_visitors_runes = { const javascript_visitors_runes = {
ClassBody(node, { state, visit, next }) { ClassBody(node, { state, visit }) {
if (!state.analysis.runes) { /** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
next(); const public_derived = new Map();
}
/** @type {import('estree').PropertyDefinition[]} */ /** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const deriveds = []; const private_derived = new Map();
/** @type {import('estree').MethodDefinition | null} */
let constructor = null; /** @type {string[]} */
// Get the constructor const private_ids = [];
for (const definition of node.body) { for (const definition of node.body) {
if (definition.type === 'MethodDefinition' && definition.kind === 'constructor') { if (
constructor = /** @type {import('estree').MethodDefinition} */ (visit(definition)); definition.type === 'PropertyDefinition' &&
} (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
} ) {
// Move $derived() runes to the end of the body if there is a constructor const { type, name } = definition.key;
if (constructor !== null) {
const body = []; const is_private = type === 'PrivateIdentifier';
for (const definition of node.body) { if (is_private) private_ids.push(name);
if (
definition.type === 'PropertyDefinition' && if (definition.value?.type === 'CallExpression') {
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') const rune = get_rune(definition.value, state.scope);
) { if (rune === '$derived' || rune === '$derived.by') {
const is_private = definition.key.type === 'PrivateIdentifier'; /** @type {import('../../3-transform/client/types.js').StateField} */
const field = {
if (definition.value?.type === 'CallExpression') { kind: rune === '$derived.by' ? 'derived_call' : 'derived',
const rune = get_rune(definition.value, state.scope); // @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) { if (is_private) {
// Keep the private #name initializer if private, but remove initial value private_derived.set(name, field);
body.push({ } else {
...definition, public_derived.set(name, field);
value: null
});
}
continue;
} }
} }
} }
if (definition.type !== 'MethodDefinition' || definition.kind !== 'constructor') {
body.push(
/** @type {import('estree').PropertyDefinition | import('estree').MethodDefinition | import('estree').StaticBlock} */ (
visit(definition)
)
);
}
} }
if (deriveds.length > 0) { }
body.push({
...constructor, // each `foo = $derived()` needs a backing `#foo` field
value: { for (const [name, field] of public_derived) {
...constructor.value, let deconflicted = name;
body: b.block([ while (private_ids.includes(deconflicted)) {
...constructor.value.body.body, deconflicted = '_' + deconflicted;
...deriveds.map((d) => { }
return b.stmt(
b.assignment( private_ids.push(deconflicted);
'=', field.id = b.private_id(deconflicted);
b.member(b.this, d.key), }
/** @type {import('estree').Expression} */ (d.value)
) /** @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(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
} }
});
} else { continue;
body.push(constructor); }
} }
return {
...node, body.push(/** @type {import('estree').MethodDefinition} **/ (visit(definition, child_state)));
body
};
} }
next();
return { ...node, body };
}, },
PropertyDefinition(node, { state, next, visit }) { PropertyDefinition(node, { state, next, visit }) {
if (node.value != null && node.value.type === 'CallExpression') { if (node.value != null && node.value.type === 'CallExpression') {
@ -730,6 +760,16 @@ const javascript_visitors_runes = {
return transform_inspect_rune(node, context); 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(); context.next();
} }
}; };
@ -2089,7 +2129,8 @@ export function server_component(analysis, options) {
metadata: { metadata: {
namespace: options.namespace namespace: options.namespace
}, },
preserve_whitespace: options.preserveWhitespace preserve_whitespace: options.preserveWhitespace,
private_derived: new Map()
}; };
const module = /** @type {import('estree').Program} */ ( 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 // 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 // to be present for `javascript_visitors` and so is included in module
// transform state as well as component transform state // 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} */ ( const module = /** @type {import('estree').Program} */ (

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

@ -8,6 +8,7 @@ import {
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../constants.js'; } from '../../constants.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../client/constants.js';
export * from '../client/validate.js'; export * from '../client/validate.js';
@ -666,3 +667,17 @@ export function loop_guard(timeout) {
export function inspect(args, inspect = console.log) { export function inspect(args, inspect = console.log) {
inspect('init', ...args); 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