Skip to content

Commit a57868e

Browse files
authored
fix: don't preserve reactivity context across function boundaries (#17002)
Fixes #16809 We gotta take into account function boundaries when determining whether or not we're inside a derived (or const).
1 parent f549478 commit a57868e

File tree

11 files changed

+143
-19
lines changed

11 files changed

+143
-19
lines changed

.changeset/short-banks-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: don't preserve reactivity context across function boundaries

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export function analyze_module(source, options) {
306306
fragment: null,
307307
parent_element: null,
308308
reactive_statement: null,
309-
in_derived: false
309+
derived_function_depth: -1
310310
},
311311
visitors
312312
);
@@ -703,7 +703,7 @@ export function analyze_component(root, source, options) {
703703
state_fields: new Map(),
704704
function_depth: scope.function_depth,
705705
reactive_statement: null,
706-
in_derived: false
706+
derived_function_depth: -1
707707
};
708708

709709
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@@ -771,7 +771,7 @@ export function analyze_component(root, source, options) {
771771
expression: null,
772772
state_fields: new Map(),
773773
function_depth: scope.function_depth,
774-
in_derived: false
774+
derived_function_depth: -1
775775
};
776776

777777
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

packages/svelte/src/compiler/phases/2-analyze/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export interface AnalysisState {
2929
reactive_statement: null | ReactiveStatement;
3030

3131
/**
32-
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
32+
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
3333
*/
34-
in_derived: boolean;
34+
derived_function_depth: number;
3535
}
3636

3737
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export function AwaitExpression(node, context) {
1515
// b) awaits that precede other expressions in template or `$derived(...)`
1616
if (
1717
tla ||
18-
(is_reactive_expression(context.path, context.state.in_derived) &&
18+
(is_reactive_expression(
19+
context.path,
20+
context.state.derived_function_depth === context.state.function_depth
21+
) &&
1922
!is_last_evaluated_expression(context.path, node))
2023
) {
2124
context.state.analysis.pickled_awaits.add(node);
@@ -53,9 +56,7 @@ export function AwaitExpression(node, context) {
5356
* @param {boolean} in_derived
5457
*/
5558
export function is_reactive_expression(path, in_derived) {
56-
if (in_derived) {
57-
return true;
58-
}
59+
if (in_derived) return true;
5960

6061
let i = path.length;
6162

@@ -67,6 +68,7 @@ export function is_reactive_expression(path, in_derived) {
6768
parent.type === 'FunctionExpression' ||
6869
parent.type === 'FunctionDeclaration'
6970
) {
71+
// No reactive expression found between function and await
7072
return false;
7173
}
7274

@@ -83,11 +85,16 @@ export function is_reactive_expression(path, in_derived) {
8385
* @param {AST.SvelteNode[]} path
8486
* @param {Expression | SpreadElement | Property} node
8587
*/
86-
export function is_last_evaluated_expression(path, node) {
88+
function is_last_evaluated_expression(path, node) {
8789
let i = path.length;
8890

8991
while (i--) {
90-
const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]);
92+
const parent = path[i];
93+
94+
if (parent.type === 'ConstTag') {
95+
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
96+
return false;
97+
}
9198

9299
// @ts-expect-error we could probably use a neater/more robust mechanism
93100
if (parent.metadata) {

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export function CallExpression(node, context) {
248248
context.next({
249249
...context.state,
250250
function_depth: context.state.function_depth + 1,
251-
in_derived: true,
251+
derived_function_depth: context.state.function_depth + 1,
252252
expression
253253
});
254254

packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export function ConstTag(node, context) {
3838
context.visit(declaration.init, {
3939
...context.state,
4040
expression: node.metadata.expression,
41-
in_derived: true
41+
// We're treating this like a $derived under the hood
42+
function_depth: context.state.function_depth + 1,
43+
derived_function_depth: context.state.function_depth + 1
4244
});
4345
}

packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ export function VariableDeclarator(node, context) {
6464
}
6565
}
6666

67-
if (rune === '$derived') {
68-
context.visit(node.id);
69-
context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true });
70-
return;
71-
}
72-
7367
if (rune === '$props') {
7468
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
7569
e.props_invalid_identifier(node);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from '../../test';
2+
3+
export default test({ compileOptions: { experimental: { async: true } } });
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'svelte/internal/disclose-version';
2+
import 'svelte/internal/flags/async';
3+
import * as $ from 'svelte/internal/client';
4+
5+
export default function Async_in_derived($$anchor, $$props) {
6+
$.push($$props, true);
7+
8+
$.async_body($$anchor, async ($$anchor) => {
9+
let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
10+
let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
11+
12+
let no1 = $.derived(async () => {
13+
return await 1;
14+
});
15+
16+
let no2 = $.derived(() => async () => {
17+
return await 1;
18+
});
19+
20+
if ($.aborted()) return;
21+
22+
var fragment = $.comment();
23+
var node = $.first_child(fragment);
24+
25+
{
26+
var consequent = ($$anchor) => {
27+
$.async_body($$anchor, async ($$anchor) => {
28+
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
29+
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
30+
31+
const no1 = $.derived(() => (async () => {
32+
return await 1;
33+
})());
34+
35+
const no2 = $.derived(() => (async () => {
36+
return await 1;
37+
})());
38+
39+
if ($.aborted()) return;
40+
});
41+
};
42+
43+
$.if(node, ($$render) => {
44+
if (true) $$render(consequent);
45+
});
46+
}
47+
48+
$.append($$anchor, fragment);
49+
});
50+
51+
$.pop();
52+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'svelte/internal/flags/async';
2+
import * as $ from 'svelte/internal/server';
3+
4+
export default function Async_in_derived($$renderer, $$props) {
5+
$$renderer.component(($$renderer) => {
6+
$$renderer.async(async ($$renderer) => {
7+
let yes1 = (await $.save(1))();
8+
let yes2 = foo((await $.save(1))());
9+
10+
let no1 = (async () => {
11+
return await 1;
12+
})();
13+
14+
let no2 = async () => {
15+
return await 1;
16+
};
17+
18+
$$renderer.async(async ($$renderer) => {
19+
if (true) {
20+
$$renderer.push('<!--[-->');
21+
22+
const yes1 = (await $.save(1))();
23+
const yes2 = foo((await $.save(1))());
24+
25+
const no1 = (async () => {
26+
return await 1;
27+
})();
28+
29+
const no2 = (async () => {
30+
return await 1;
31+
})();
32+
} else {
33+
$$renderer.push('<!--[!-->');
34+
}
35+
});
36+
37+
$$renderer.push(`<!--]-->`);
38+
});
39+
});
40+
}

0 commit comments

Comments
 (0)