diff --git a/.changeset/short-banks-yell.md b/.changeset/short-banks-yell.md new file mode 100644 index 000000000000..34d5ba66d326 --- /dev/null +++ b/.changeset/short-banks-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't preserve reactivity context across function boundaries diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 47fe37c44df9..52be9973748e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -306,7 +306,7 @@ export function analyze_module(source, options) { fragment: null, parent_element: null, reactive_statement: null, - in_derived: false + derived_function_depth: -1 }, visitors ); @@ -703,7 +703,7 @@ export function analyze_component(root, source, options) { state_fields: new Map(), function_depth: scope.function_depth, reactive_statement: null, - in_derived: false + derived_function_depth: -1 }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); @@ -771,7 +771,7 @@ export function analyze_component(root, source, options) { expression: null, state_fields: new Map(), function_depth: scope.function_depth, - in_derived: false + derived_function_depth: -1 }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index ae9c5911f64b..bad6c7d6131c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -29,9 +29,9 @@ export interface AnalysisState { reactive_statement: null | ReactiveStatement; /** - * True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`) + * Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const` */ - in_derived: boolean; + derived_function_depth: number; } export type Context = import('zimmerframe').Context< diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 90186235708a..14757af4a3c8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -15,7 +15,10 @@ export function AwaitExpression(node, context) { // b) awaits that precede other expressions in template or `$derived(...)` if ( tla || - (is_reactive_expression(context.path, context.state.in_derived) && + (is_reactive_expression( + context.path, + context.state.derived_function_depth === context.state.function_depth + ) && !is_last_evaluated_expression(context.path, node)) ) { context.state.analysis.pickled_awaits.add(node); @@ -53,9 +56,7 @@ export function AwaitExpression(node, context) { * @param {boolean} in_derived */ export function is_reactive_expression(path, in_derived) { - if (in_derived) { - return true; - } + if (in_derived) return true; let i = path.length; @@ -67,6 +68,7 @@ export function is_reactive_expression(path, in_derived) { parent.type === 'FunctionExpression' || parent.type === 'FunctionDeclaration' ) { + // No reactive expression found between function and await return false; } @@ -83,11 +85,16 @@ export function is_reactive_expression(path, in_derived) { * @param {AST.SvelteNode[]} path * @param {Expression | SpreadElement | Property} node */ -export function is_last_evaluated_expression(path, node) { +function is_last_evaluated_expression(path, node) { let i = path.length; while (i--) { - const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]); + const parent = path[i]; + + if (parent.type === 'ConstTag') { + // {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment + return false; + } // @ts-expect-error we could probably use a neater/more robust mechanism if (parent.metadata) { 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 76d9cecd9ab1..4b66abe1d136 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -248,7 +248,7 @@ export function CallExpression(node, context) { context.next({ ...context.state, function_depth: context.state.function_depth + 1, - in_derived: true, + derived_function_depth: context.state.function_depth + 1, expression }); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index 5849d828a3de..77ea6549054b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -38,6 +38,8 @@ export function ConstTag(node, context) { context.visit(declaration.init, { ...context.state, expression: node.metadata.expression, - in_derived: true + // We're treating this like a $derived under the hood + function_depth: context.state.function_depth + 1, + derived_function_depth: context.state.function_depth + 1 }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index 7a85b4a93aa1..dfb1d54040fd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -64,12 +64,6 @@ export function VariableDeclarator(node, context) { } } - if (rune === '$derived') { - context.visit(node.id); - context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true }); - return; - } - if (rune === '$props') { if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { e.props_invalid_identifier(node); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js new file mode 100644 index 000000000000..2e30bbeb1618 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js new file mode 100644 index 000000000000..7a97850175a6 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -0,0 +1,52 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_in_derived($$anchor, $$props) { + $.push($$props, true); + + $.async_body($$anchor, async ($$anchor) => { + let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + + let no1 = $.derived(async () => { + return await 1; + }); + + let no2 = $.derived(() => async () => { + return await 1; + }); + + if ($.aborted()) return; + + var fragment = $.comment(); + var node = $.first_child(fragment); + + { + var consequent = ($$anchor) => { + $.async_body($$anchor, async ($$anchor) => { + const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + + const no1 = $.derived(() => (async () => { + return await 1; + })()); + + const no2 = $.derived(() => (async () => { + return await 1; + })()); + + if ($.aborted()) return; + }); + }; + + $.if(node, ($$render) => { + if (true) $$render(consequent); + }); + } + + $.append($$anchor, fragment); + }); + + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js new file mode 100644 index 000000000000..69eca5a38390 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -0,0 +1,40 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_in_derived($$renderer, $$props) { + $$renderer.component(($$renderer) => { + $$renderer.async(async ($$renderer) => { + let yes1 = (await $.save(1))(); + let yes2 = foo((await $.save(1))()); + + let no1 = (async () => { + return await 1; + })(); + + let no2 = async () => { + return await 1; + }; + + $$renderer.async(async ($$renderer) => { + if (true) { + $$renderer.push(''); + + const yes1 = (await $.save(1))(); + const yes2 = foo((await $.save(1))()); + + const no1 = (async () => { + return await 1; + })(); + + const no2 = (async () => { + return await 1; + })(); + } else { + $$renderer.push(''); + } + }); + + $$renderer.push(``); + }); + }); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte b/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte new file mode 100644 index 000000000000..bda88fd3ae21 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte @@ -0,0 +1,21 @@ + + +{#if true} + {@const yes1 = await 1} + {@const yes2 = foo(await 1)} + {@const no1 = (async () => { + return await 1; + })()} + {@const no2 = (async () => { + return await 1; + })()} +{/if}