Skip to content

Commit b3eeee0

Browse files
committed
fix: thunkify deriveds on the server
1 parent 76ce303 commit b3eeee0

File tree

9 files changed

+159
-12
lines changed

9 files changed

+159
-12
lines changed

.changeset/curvy-toes-warn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: thunkify deriveds on the server

packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ export function ClassBody(node, context) {
8585
const init = /** @type {Expression} **/ (
8686
context.visit(definition.value.arguments[0], child_state)
8787
);
88-
const value =
89-
field.kind === 'derived_by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
88+
const value = field.kind === 'derived_by' ? init : b.thunk(init);
9089

9190
if (is_private) {
9291
body.push(b.prop_def(field.id, value));

packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js

+54-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
1+
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier, ObjectPattern, ArrayPattern, Property } from 'estree' */
22
/** @import { Binding } from '#compiler' */
33
/** @import { Context } from '../types.js' */
44
/** @import { Scope } from '../../../scope.js' */
5-
import { build_fallback, extract_paths } from '../../../../utils/ast.js';
5+
import { walk } from 'zimmerframe';
6+
import { build_fallback, extract_identifiers, extract_paths } from '../../../../utils/ast.js';
67
import * as b from '../../../../utils/builders.js';
78
import { get_rune } from '../../../scope.js';
8-
import { walk } from 'zimmerframe';
99

1010
/**
1111
* @param {VariableDeclaration} node
@@ -16,6 +16,9 @@ export function VariableDeclaration(node, context) {
1616
const declarations = [];
1717

1818
if (context.state.analysis.runes) {
19+
/** @type {VariableDeclarator[]} */
20+
const destructured_reassigns = [];
21+
1922
for (const declarator of node.declarations) {
2023
const init = declarator.init;
2124
const rune = get_rune(init, context.state.scope);
@@ -73,27 +76,72 @@ export function VariableDeclaration(node, context) {
7376
const value =
7477
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
7578

79+
const is_destructuring =
80+
declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern';
81+
82+
/**
83+
*
84+
* @param {()=>Expression} get_generated_init
85+
*/
86+
function add_destructured_reassign(get_generated_init) {
87+
// to keep everything that the user destructure as a function we need to change the original
88+
// assignment to a generated value and then reassign a variable with the original name
89+
if (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') {
90+
const id = /** @type {ObjectPattern | ArrayPattern} */ (context.visit(declarator.id));
91+
const modified = walk(
92+
/**@type {Identifier|Property}*/ (/**@type {unknown}*/ (id)),
93+
{},
94+
{
95+
Identifier(id, { path }) {
96+
const parent = path.at(-1);
97+
// we only want the identifiers for the value
98+
if (parent?.type === 'Property' && parent.value !== id) return;
99+
const generated = context.state.scope.generate(id.name);
100+
destructured_reassigns.push(b.declarator(b.id(id.name), b.thunk(b.id(generated))));
101+
return b.id(generated);
102+
}
103+
}
104+
);
105+
declarations.push(b.declarator(/**@type {Pattern}*/ (modified), get_generated_init()));
106+
}
107+
}
108+
76109
if (rune === '$derived.by') {
110+
if (is_destructuring) {
111+
add_destructured_reassign(() => b.call(value));
112+
continue;
113+
}
77114
declarations.push(
78-
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
115+
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
79116
);
80117
continue;
81118
}
82119

83120
if (declarator.id.type === 'Identifier') {
84-
declarations.push(b.declarator(declarator.id, value));
121+
if (is_destructuring && rune === '$derived') {
122+
add_destructured_reassign(() => value);
123+
continue;
124+
}
125+
declarations.push(
126+
b.declarator(declarator.id, rune === '$derived' ? b.thunk(value) : value)
127+
);
85128
continue;
86129
}
87130

88131
if (rune === '$derived') {
132+
if (is_destructuring) {
133+
add_destructured_reassign(() => value);
134+
continue;
135+
}
89136
declarations.push(
90-
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
137+
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.thunk(value))
91138
);
92139
continue;
93140
}
94141

95142
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
96143
}
144+
declarations.push(...destructured_reassigns);
97145
} else {
98146
for (const declarator of node.declarations) {
99147
const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator));

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js

+4
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ export function build_getter(node, state) {
237237
b.literal(node.name),
238238
build_getter(store_id, state)
239239
);
240+
} else if (binding.kind === 'derived') {
241+
// we need a maybe_call because in case of `var`
242+
// the user might use the variable before the initialization
243+
return b.maybe_call(node.name);
240244
}
241245

242246
return node;

packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import * as $ from 'svelte/internal/server';
22

33
export default function Await_block_scope($$payload) {
44
let counter = { count: 0 };
5-
const promise = Promise.resolve(counter);
5+
const promise = () => Promise.resolve(counter);
66

77
function increment() {
88
counter.count += 1;
99
}
1010

1111
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> <!---->`;
12-
$.await(promise, () => {}, (counter) => {}, () => {});
12+
$.await(promise?.(), () => {}, (counter) => {}, () => {});
1313
$$payload.out += `<!----> ${$.escape(counter.count)}`;
1414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
mode: ['server']
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as $ from "svelte/internal/server";
2+
3+
export default function Server_deriveds($$payload, $$props) {
4+
$.push();
5+
6+
// destructuring stuff on the server needs a bit more code
7+
// so that every identifier is a function
8+
let stuff = {
9+
foo: true,
10+
bar: [1, 2, { baz: 'baz' }]
11+
};
12+
13+
let {
14+
foo: foo_1,
15+
bar: [a_1, b_1, { baz: baz_1 }]
16+
} = stuff,
17+
foo = () => foo_1,
18+
a = () => a_1,
19+
b = () => b_1,
20+
baz = () => baz_1;
21+
22+
let stuff2 = [1, 2, 3];
23+
24+
let [d_1, e_1, f_1] = stuff2,
25+
d = () => d_1,
26+
e = () => e_1,
27+
f = () => f_1;
28+
29+
let count = 0;
30+
let double = () => count * 2;
31+
let identifier = () => count;
32+
let dot_by = () => () => count;
33+
34+
class Test {
35+
state = 0;
36+
#der = () => this.state * 2;
37+
38+
get der() {
39+
return this.#der();
40+
}
41+
42+
#der_by = () => this.state;
43+
44+
get der_by() {
45+
return this.#der_by();
46+
}
47+
48+
#identifier = () => this.state;
49+
50+
get identifier() {
51+
return this.#identifier();
52+
}
53+
}
54+
55+
const test = new Test();
56+
57+
$$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)}`;
58+
$.pop();
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
// destructuring stuff on the server needs a bit more code
3+
// so that every identifier is a function
4+
let stuff = $state({ foo: true, bar: [1, 2, {baz: 'baz'}] });
5+
let { foo, bar: [a, b, { baz }]} = $derived(stuff);
6+
7+
let stuff2 = $state([1, 2, 3]);
8+
let [d, e, f] = $derived(stuff2);
9+
10+
let count = $state(0);
11+
let double = $derived(count * 2);
12+
let identifier = $derived(count);
13+
let dot_by = $derived(()=>count);
14+
15+
class Test{
16+
state = $state(0);
17+
der = $derived(this.state * 2);
18+
der_by = $derived.by(()=>this.state);
19+
identifier = $derived(this.state);
20+
}
21+
22+
const test = new Test();
23+
</script>
24+
25+
{foo} {a} {b} {baz} {d} {e} {f} {double} {identifier} {dot_by} {test.der} {test.der_by} {test.identifier}

packages/svelte/tests/snapshot/test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { VERSION } from 'svelte/compiler';
77

88
interface SnapshotTest extends BaseTest {
99
compileOptions?: Partial<import('#compiler').CompileOptions>;
10+
mode?: ('client' | 'server')[];
1011
}
1112

1213
const { test, run } = suite<SnapshotTest>(async (config, cwd) => {
13-
await compile_directory(cwd, 'client', config.compileOptions);
14-
await compile_directory(cwd, 'server', config.compileOptions);
14+
for (const mode of config?.mode ?? ['server', 'client']) {
15+
await compile_directory(cwd, mode, config.compileOptions);
16+
}
1517

1618
// run `UPDATE_SNAPSHOTS=true pnpm test snapshot` to update snapshot tests
1719
if (process.env.UPDATE_SNAPSHOTS) {

0 commit comments

Comments
 (0)