fix: thunkify deriveds on the server

thunkify-deriveds-on-server
paoloricciuti 2 weeks ago
parent 76ce303b0a
commit b3eeee08b6

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

@ -85,8 +85,7 @@ export function ClassBody(node, context) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
const value = field.kind === 'derived_by' ? init : b.thunk(init);
if (is_private) {
body.push(b.prop_def(field.id, value));

@ -1,11 +1,11 @@
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier, ObjectPattern, ArrayPattern, Property } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { build_fallback, extract_paths } from '../../../../utils/ast.js';
import { walk } from 'zimmerframe';
import { build_fallback, extract_identifiers, extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
/**
* @param {VariableDeclaration} node
@ -16,6 +16,9 @@ export function VariableDeclaration(node, context) {
const declarations = [];
if (context.state.analysis.runes) {
/** @type {VariableDeclarator[]} */
const destructured_reassigns = [];
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, context.state.scope);
@ -73,27 +76,72 @@ export function VariableDeclaration(node, context) {
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
const is_destructuring =
declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern';
/**
*
* @param {()=>Expression} get_generated_init
*/
function add_destructured_reassign(get_generated_init) {
// to keep everything that the user destructure as a function we need to change the original
// assignment to a generated value and then reassign a variable with the original name
if (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') {
const id = /** @type {ObjectPattern | ArrayPattern} */ (context.visit(declarator.id));
const modified = walk(
/**@type {Identifier|Property}*/ (/**@type {unknown}*/ (id)),
{},
{
Identifier(id, { path }) {
const parent = path.at(-1);
// we only want the identifiers for the value
if (parent?.type === 'Property' && parent.value !== id) return;
const generated = context.state.scope.generate(id.name);
destructured_reassigns.push(b.declarator(b.id(id.name), b.thunk(b.id(generated))));
return b.id(generated);
}
}
);
declarations.push(b.declarator(/**@type {Pattern}*/ (modified), get_generated_init()));
}
}
if (rune === '$derived.by') {
if (is_destructuring) {
add_destructured_reassign(() => b.call(value));
continue;
}
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
);
continue;
}
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, value));
if (is_destructuring && rune === '$derived') {
add_destructured_reassign(() => value);
continue;
}
declarations.push(
b.declarator(declarator.id, rune === '$derived' ? b.thunk(value) : value)
);
continue;
}
if (rune === '$derived') {
if (is_destructuring) {
add_destructured_reassign(() => value);
continue;
}
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.thunk(value))
);
continue;
}
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
}
declarations.push(...destructured_reassigns);
} else {
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator));

@ -237,6 +237,10 @@ export function build_getter(node, state) {
b.literal(node.name),
build_getter(store_id, state)
);
} else if (binding.kind === 'derived') {
// we need a maybe_call because in case of `var`
// the user might use the variable before the initialization
return b.maybe_call(node.name);
}
return node;

@ -2,13 +2,13 @@ import * as $ from 'svelte/internal/server';
export default function Await_block_scope($$payload) {
let counter = { count: 0 };
const promise = Promise.resolve(counter);
const promise = () => Promise.resolve(counter);
function increment() {
counter.count += 1;
}
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> <!---->`;
$.await(promise, () => {}, (counter) => {}, () => {});
$.await(promise?.(), () => {}, (counter) => {}, () => {});
$$payload.out += `<!----> ${$.escape(counter.count)}`;
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
mode: ['server']
});

@ -0,0 +1,59 @@
import * as $ from "svelte/internal/server";
export default function Server_deriveds($$payload, $$props) {
$.push();
// destructuring stuff on the server needs a bit more code
// so that every identifier is a function
let stuff = {
foo: true,
bar: [1, 2, { baz: 'baz' }]
};
let {
foo: foo_1,
bar: [a_1, b_1, { baz: baz_1 }]
} = stuff,
foo = () => foo_1,
a = () => a_1,
b = () => b_1,
baz = () => baz_1;
let stuff2 = [1, 2, 3];
let [d_1, e_1, f_1] = stuff2,
d = () => d_1,
e = () => e_1,
f = () => f_1;
let count = 0;
let double = () => count * 2;
let identifier = () => count;
let dot_by = () => () => count;
class Test {
state = 0;
#der = () => this.state * 2;
get der() {
return this.#der();
}
#der_by = () => this.state;
get der_by() {
return this.#der_by();
}
#identifier = () => this.state;
get identifier() {
return this.#identifier();
}
}
const test = new Test();
$$payload.out += `<!---->${$.escape(foo?.())} ${$.escape(a?.())} ${$.escape(b?.())} ${$.escape(baz?.())} ${$.escape(d?.())} ${$.escape(e?.())} ${$.escape(f?.())} ${$.escape(double?.())} ${$.escape(identifier?.())} ${$.escape(dot_by?.())} ${$.escape(test.der)} ${$.escape(test.der_by)} ${$.escape(test.identifier)}`;
$.pop();
}

@ -0,0 +1,25 @@
<script>
// destructuring stuff on the server needs a bit more code
// so that every identifier is a function
let stuff = $state({ foo: true, bar: [1, 2, {baz: 'baz'}] });
let { foo, bar: [a, b, { baz }]} = $derived(stuff);
let stuff2 = $state([1, 2, 3]);
let [d, e, f] = $derived(stuff2);
let count = $state(0);
let double = $derived(count * 2);
let identifier = $derived(count);
let dot_by = $derived(()=>count);
class Test{
state = $state(0);
der = $derived(this.state * 2);
der_by = $derived.by(()=>this.state);
identifier = $derived(this.state);
}
const test = new Test();
</script>
{foo} {a} {b} {baz} {d} {e} {f} {double} {identifier} {dot_by} {test.der} {test.der_by} {test.identifier}

@ -7,11 +7,13 @@ import { VERSION } from 'svelte/compiler';
interface SnapshotTest extends BaseTest {
compileOptions?: Partial<import('#compiler').CompileOptions>;
mode?: ('client' | 'server')[];
}
const { test, run } = suite<SnapshotTest>(async (config, cwd) => {
await compile_directory(cwd, 'client', config.compileOptions);
await compile_directory(cwd, 'server', config.compileOptions);
for (const mode of config?.mode ?? ['server', 'client']) {
await compile_directory(cwd, mode, config.compileOptions);
}
// run `UPDATE_SNAPSHOTS=true pnpm test snapshot` to update snapshot tests
if (process.env.UPDATE_SNAPSHOTS) {

Loading…
Cancel
Save