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/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index adde7480cbd1..c9000ca4876c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -6,6 +6,7 @@ import type { ComponentAnalysis } from '../../types.js'; export interface ServerTransformState extends TransformState { /** The $: calls, which will be ordered in the end */ readonly legacy_reactive_statements: Map; + readonly is_destructured_derived?: boolean; } export interface ComponentServerTransformState extends ServerTransformState { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 35c79988b08b..daafdc9295cc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -31,7 +31,12 @@ export function CallExpression(node, context) { if (rune === '$derived' || rune === '$derived.by') { const fn = /** @type {Expression} */ (context.visit(node.arguments[0])); - return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); + + return b.call( + '$.derived', + rune === '$derived' ? b.thunk(fn) : fn, + context.state.is_destructured_derived && b.true + ); } if (rune === '$state.snapshot') { 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 7c945951471c..5fc5a259b1c7 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 @@ -81,22 +81,21 @@ export function VariableDeclaration(node, context) { continue; } - const args = /** @type {CallExpression} */ (init).arguments; - const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; - - if (rune === '$derived.by') { - declarations.push( - b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value)) - ); - continue; - } + const value = init + ? /** @type {Expression} */ ( + context.visit(init, { + ...context.state, + is_destructured_derived: declarator.id.type !== 'Identifier' + }) + ) + : b.void0; if (declarator.id.type === 'Identifier') { declarations.push(b.declarator(declarator.id, value)); continue; } - if (rune === '$derived') { + if (rune === '$derived' || rune === '$derived.by') { declarations.push( b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 7207564ef983..f61e8fab6a83 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -163,6 +163,19 @@ export function build_element_attributes(node, context) { ]) ); } else { + /** @type {Expression} */ + let expression = attribute.expression; + + if (attribute.type === 'BindDirective' && expression.type === 'SequenceExpression') { + const getter = expression.expressions[0]; + expression = + getter.type === 'ArrowFunctionExpression' && + getter.params.length === 0 && + getter.body.type !== 'BlockStatement' + ? getter.body + : b.call(getter); + } + attributes.push( create_attribute(attribute.name, -1, -1, [ { 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 8fcf8efa68b6..bbc81d55c590 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 @@ -239,6 +239,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/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2ca85fff44c2..56f31e10d023 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -3,7 +3,7 @@ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; -import { is_promise, noop } from '../shared/utils.js'; +import { access_path_on_object, is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { UNINITIALIZED, @@ -18,6 +18,7 @@ import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; import { Payload } from './payload.js'; +import { is } from '../client/proxy.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -518,15 +519,55 @@ export { escape_html as escape }; /** * @template T * @param {()=>T} fn - * @returns {(new_value?: T) => (T | void)} + * @param {boolean} is_destructured + * @returns {((new_value?: T) => (T | void)) | Record (T | void)>} */ -export function derived(fn) { +export function derived(fn, is_destructured = false) { const get_value = once(fn); /** * @type {T | undefined} */ let updated_value; + if (is_destructured) { + /** + * + * @param {(string | symbol)[]} path + * @returns + */ + function recursive_proxy(path = []) { + return new Proxy( + /** @type {(new_value: any)=>any} */ ( + function (new_value) { + if (arguments.length === 0) { + return ( + access_path_on_object(/** @type {*} */ (updated_value), path) ?? + access_path_on_object(/** @type {*} */ (get_value()), path) + ); + } + var last_key = path[path.length - 1]; + const to_update = access_path_on_object( + /** @type {*} */ (updated_value), + path.slice(0, -1), + (current, key) => { + current[key] = {}; + } + ); + /** @type {*} */ (to_update)[last_key] = new_value; + return /** @type {*} */ (to_update)[last_key]; + } + ), + { + get(_, key) { + return recursive_proxy([...path, key]); + } + } + ); + } + updated_value = /** @type {T} */ ({}); + return recursive_proxy(); + } + return function (new_value) { if (arguments.length === 0) { return updated_value ?? get_value(); diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 10f8597520d9..10c218f7e648 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -116,3 +116,25 @@ export function to_array(value, n) { return array; } + +/** + * + * @param {Record} obj + * @param {(string | symbol)[]} path + * @param {(current: Record, key:string|symbol)=>void} [on_undefined] + * @returns + */ +export function access_path_on_object(obj, path, on_undefined) { + if (obj == null) return undefined; + + let current = obj; + for (const key of path) { + if (current == null) return undefined; + if (current[key] == null && on_undefined) { + on_undefined(current, key); + } + current = /** @type {*} */ (current)[key]; + } + + return current; +} 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 4b6e32d58e0a..139dd2e13236 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 = $.derived(() => Promise.resolve(counter)); function increment() { counter.count += 1; } $$payload.out += ` `; - $.await($$payload, promise, () => {}, (counter) => {}); + $.await($$payload, 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..ce72a3d2fdc0 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/server-deriveds/_expected/server/index.svelte.js @@ -0,0 +1,54 @@ +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, bar: [a, b, { baz }] } = $.derived(() => stuff, true); + let stuff2 = [1, 2, 3]; + let [d, e, f] = $.derived(() => stuff2, true); + let count = 0; + let double = $.derived(() => count * 2); + let identifier = $.derived(() => count); + let dot_by = $.derived(() => () => count); + + class Test { + state = 0; + #der = $.derived(() => this.state * 2); + + get der() { + return this.#der(); + } + + set der($$value) { + return this.#der($$value); + } + + #der_by = $.derived(() => this.state); + + get der_by() { + return this.#der_by(); + } + + set der_by($$value) { + return this.#der_by($$value); + } + + #identifier = $.derived(() => this.state); + + get identifier() { + return this.#identifier(); + } + + set identifier($$value) { + return this.#identifier($$value); + } + } + + const test = new Test(); + + $$payload.out += `${$.escape(foo?.())} ${$.escape(a?.())} ${$.escape(b?.())} ${$.escape(baz?.())} ${$.escape(d?.())} ${$.escape(e?.())} ${$.escape(f?.())} 0 0 ${$.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 0a591c6e2a71..3a0f1f32a1e3 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) {