From e846ebb5785075dbe796d90758d58551a55da50b Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Fri, 28 Nov 2025 23:06:13 +0100 Subject: [PATCH] feat: allow `$derived($state())` for deep reactive deriveds --- .changeset/cyan-planes-yawn.md | 5 +++ .../2-analyze/visitors/CallExpression.js | 10 ++++- .../client/visitors/CallExpression.js | 22 ++++++++++- .../derived-state-init-class/Class.svelte.js | 6 +++ .../derived-state-init-class/Component.svelte | 13 +++++++ .../derived-state-init-class/_config.js | 37 +++++++++++++++++++ .../derived-state-init-class/main.svelte | 10 +++++ .../derived-state-init/Component.svelte | 12 ++++++ .../samples/derived-state-init/_config.js | 37 +++++++++++++++++++ .../samples/derived-state-init/main.svelte | 10 +++++ 10 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 .changeset/cyan-planes-yawn.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Class.svelte.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init-class/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init-class/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-state-init/main.svelte diff --git a/.changeset/cyan-planes-yawn.md b/.changeset/cyan-planes-yawn.md new file mode 100644 index 0000000000..48731088e0 --- /dev/null +++ b/.changeset/cyan-planes-yawn.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow `$derived($state())` for deep reactive deriveds diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 52eba8c735..8299576ae8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -121,7 +121,15 @@ export function CallExpression(node, context) { is_class_property_definition(parent) || is_class_property_assignment_at_constructor_root(parent, context); - if (!valid) { + if ( + !valid && + (rune !== '$state' || + !( + parent.type === 'CallExpression' && + parent.callee.type === 'Identifier' && + parent.callee.name === '$derived' + )) + ) { e.state_invalid_placement(node, rune); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index f69bc5fe6e..9db88c21e5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -5,6 +5,7 @@ import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { should_proxy } from '../utils.js'; import { get_inspect_args } from '../../utils.js'; +import { get_parent } from '../../../../utils/ast.js'; /** * @param {CallExpression} node @@ -12,6 +13,7 @@ import { get_inspect_args } from '../../utils.js'; */ export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + const parent = get_parent(context.path, -1); switch (rune) { case '$host': @@ -40,7 +42,25 @@ export function CallExpression(node, context) { } const callee = b.id('$.state', node.callee.loc); - return b.call(callee, value); + let retval = b.call(callee, value); + + // this is not the path you would expect from `$derived($state(...))`, but + // it looks like this because we visit the arguments of the derived in the + // VariableDeclaration visitor + if ( + (parent.type === 'VariableDeclaration' && + parent.declarations.length === 1 && + parent.declarations[0].init?.type === 'CallExpression' && + parent.declarations[0].init.callee.type === 'Identifier' && + parent.declarations[0].init?.callee.name === '$derived') || + (parent.type === 'CallExpression' && + parent.callee.type === 'Identifier' && + parent.callee.name === '$derived') + ) { + retval = b.call('$.get', retval); + } + + return retval; } case '$derived': diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Class.svelte.js b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Class.svelte.js new file mode 100644 index 0000000000..df9c199111 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Class.svelte.js @@ -0,0 +1,6 @@ +export class Test { + local_arr; + constructor(arr) { + this.local_arr = $derived($state(arr())); + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Component.svelte b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Component.svelte new file mode 100644 index 0000000000..c13218ec8f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/Component.svelte @@ -0,0 +1,13 @@ + + + +{#each test.local_arr as item} +

{item}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/_config.js new file mode 100644 index 0000000000..5702a8554a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/_config.js @@ -0,0 +1,37 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + const [update_parent, push] = target.querySelectorAll('button'); + let p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 3); + assert.equal(p_tags[0].textContent, '1'); + assert.equal(p_tags[1].textContent, '2'); + assert.equal(p_tags[2].textContent, '3'); + + flushSync(() => { + push.click(); + }); + + p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 4); + assert.equal(p_tags[0].textContent, '1'); + assert.equal(p_tags[1].textContent, '2'); + assert.equal(p_tags[2].textContent, '3'); + assert.equal(p_tags[3].textContent, '4'); + + flushSync(() => { + update_parent.click(); + }); + + p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 3); + assert.equal(p_tags[0].textContent, '4'); + assert.equal(p_tags[1].textContent, '5'); + assert.equal(p_tags[2].textContent, '6'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/main.svelte new file mode 100644 index 0000000000..848fed2b81 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init-class/main.svelte @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init/Component.svelte b/packages/svelte/tests/runtime-runes/samples/derived-state-init/Component.svelte new file mode 100644 index 0000000000..c73ebc8c93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init/Component.svelte @@ -0,0 +1,12 @@ + + + +{#each local_arr as item} +

{item}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-state-init/_config.js new file mode 100644 index 0000000000..5702a8554a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init/_config.js @@ -0,0 +1,37 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + const [update_parent, push] = target.querySelectorAll('button'); + let p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 3); + assert.equal(p_tags[0].textContent, '1'); + assert.equal(p_tags[1].textContent, '2'); + assert.equal(p_tags[2].textContent, '3'); + + flushSync(() => { + push.click(); + }); + + p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 4); + assert.equal(p_tags[0].textContent, '1'); + assert.equal(p_tags[1].textContent, '2'); + assert.equal(p_tags[2].textContent, '3'); + assert.equal(p_tags[3].textContent, '4'); + + flushSync(() => { + update_parent.click(); + }); + + p_tags = target.querySelectorAll('p'); + + assert.equal(p_tags.length, 3); + assert.equal(p_tags[0].textContent, '4'); + assert.equal(p_tags[1].textContent, '5'); + assert.equal(p_tags[2].textContent, '6'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-state-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-state-init/main.svelte new file mode 100644 index 0000000000..848fed2b81 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-state-init/main.svelte @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file