diff --git a/.changeset/declaration-tag-multiple-declarators.md b/.changeset/declaration-tag-multiple-declarators.md new file mode 100644 index 0000000000..3f17162b54 --- /dev/null +++ b/.changeset/declaration-tag-multiple-declarators.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`) diff --git a/.changeset/declaration-tag-state-referenced-locally.md b/.changeset/declaration-tag-state-referenced-locally.md new file mode 100644 index 0000000000..00803c3e1c --- /dev/null +++ b/.changeset/declaration-tag-state-referenced-locally.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags diff --git a/.changeset/declaration-tag-whitespace.md b/.changeset/declaration-tag-whitespace.md new file mode 100644 index 0000000000..b64d1dd5a1 --- /dev/null +++ b/.changeset/declaration-tag-whitespace.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: tolerate whitespace before `let`/`const` in declaration tags diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js index e75c05f355..b5e8c931f1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js @@ -2,14 +2,12 @@ /** @import { Context } from '../types' */ import * as b from '#compiler/builders'; import * as e from '../../../errors.js'; -import { validate_opening_tag } from './shared/utils.js'; /** * @param {AST.DeclarationTag} node * @param {Context} context */ export function DeclarationTag(node, context) { - validate_opening_tag(node, context.state, node.declaration.kind[0]); if (!context.state.analysis.runes && !context.state.analysis.maybe_runes) { e.declaration_tag_no_legacy_mode(node); } @@ -17,6 +15,10 @@ export function DeclarationTag(node, context) { context.visit(node.declaration, { ...context.state, in_declaration_tag: true, + // the declaration lives in the fragment scope, which is one level deeper than the + // `function_depth` we're tracking here (`set_scope` doesn't update `function_depth`). + // align them so that `state_referenced_locally` warnings are calculated correctly + function_depth: context.state.scope.function_depth, expression: node.metadata.expression }); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js index bff51b20dd..abec5e828f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js @@ -10,8 +10,10 @@ import { add_state_transformers } from './shared/declarations.js'; * @param {ComponentContext} context */ export function DeclarationTag(node, context) { - const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration)); + // register the transformers _before_ visiting the declaration, so that + // later declarators can reference earlier ones (e.g. `{let a = $state(0), b = $derived(a * 2)}`) add_state_transformers(context); + const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration)); if ( node.metadata.promises_id && diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js new file mode 100644 index 0000000000..c5846ba0bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
count: 0
doubled: 0
quadrupled: 0
`, + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `count: 1
doubled: 2
quadrupled: 4
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte new file mode 100644 index 0000000000..d6fb542396 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte @@ -0,0 +1,10 @@ + +{let count = $state(0), doubled = $derived(count * 2)} + + +{ let quadrupled = $derived(doubled * 2) } + + +count: {count}
+doubled: {doubled}
+quadrupled: {quadrupled}
diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte new file mode 100644 index 0000000000..90cc208e72 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte @@ -0,0 +1,9 @@ + +{let a = $state(0), b = $derived(a * 2)} +{let c = $state(0)} +{let d = $derived(c * 2)} + + +{let e = $state(0), f = e} + +{a}{b}{c}{d}{e}{f} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json new file mode 100644 index 0000000000..9b87a3afb6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_referenced_locally", + "message": "This reference only captures the initial value of `e`. Did you mean to reference it inside a closure instead?", + "start": { + "line": 7, + "column": 24 + }, + "end": { + "line": 7, + "column": 25 + } + } +]