diff --git a/.changeset/curvy-toes-warn.md b/.changeset/curvy-toes-warn.md new file mode 100644 index 000000000000..07f1c7dcd468 --- /dev/null +++ b/.changeset/curvy-toes-warn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: thunkify deriveds on the server diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 365084a28486..b03e1b5ab516 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -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)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index 31de811ac76f..a530b52f7677 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -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)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2c6aa2f316aa..1a4898a3d7cb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -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; diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index 012789a5509b..34e26e7fa096 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -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 += ` `; - $.await(promise, () => {}, (counter) => {}, () => {}); + $.await(promise?.(), () => {}, (counter) => {}, () => {}); $$payload.out += ` ${$.escape(counter.count)}`; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/server-deriveds/_config.js b/packages/svelte/tests/snapshot/samples/server-deriveds/_config.js new file mode 100644 index 000000000000..2a7b882b86ea --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/server-deriveds/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + mode: ['server'] +}); diff --git a/packages/svelte/tests/snapshot/samples/server-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/server-deriveds/_expected/server/index.svelte.js new file mode 100644 index 000000000000..18b3379ed311 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/server-deriveds/_expected/server/index.svelte.js @@ -0,0 +1,56 @@ +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(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/server-deriveds/index.svelte b/packages/svelte/tests/snapshot/samples/server-deriveds/index.svelte new file mode 100644 index 000000000000..ab97da6bf8fc --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/server-deriveds/index.svelte @@ -0,0 +1,25 @@ + + +{foo} {a} {b} {baz} {d} {e} {f} {double} {identifier} {dot_by} {test.der} {test.der_by} {test.identifier} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts index 88cf9193c3f7..f0f626f7915e 100644 --- a/packages/svelte/tests/snapshot/test.ts +++ b/packages/svelte/tests/snapshot/test.ts @@ -7,11 +7,13 @@ import { VERSION } from 'svelte/compiler'; interface SnapshotTest extends BaseTest { compileOptions?: Partial; + mode?: ('client' | 'server')[]; } const { test, run } = suite(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) {